dependabot-cargo 0.81.0

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.
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "excon"
4
+ require "dependabot/metadata_finders/base"
5
+ require "dependabot/shared_helpers"
6
+
7
+ module Dependabot
8
+ module Cargo
9
+ class MetadataFinder < Dependabot::MetadataFinders::Base
10
+ SOURCE_KEYS = %w(repository homepage documentation).freeze
11
+
12
+ private
13
+
14
+ def look_up_source
15
+ case new_source_type
16
+ when "default" then find_source_from_crates_listing
17
+ when "git" then find_source_from_git_url
18
+ else raise "Unexpected source type: #{new_source_type}"
19
+ end
20
+ end
21
+
22
+ def new_source_type
23
+ sources =
24
+ dependency.requirements.map { |r| r.fetch(:source) }.uniq.compact
25
+
26
+ return "default" if sources.empty?
27
+ raise "Multiple sources! #{sources.join(', ')}" if sources.count > 1
28
+
29
+ sources.first[:type] || sources.first.fetch("type")
30
+ end
31
+
32
+ def find_source_from_crates_listing
33
+ potential_source_urls =
34
+ SOURCE_KEYS.
35
+ map { |key| crates_listing.dig("crate", key) }.
36
+ compact
37
+
38
+ source_url = potential_source_urls.find { |url| Source.from_url(url) }
39
+ Source.from_url(source_url)
40
+ end
41
+
42
+ def find_source_from_git_url
43
+ info = dependency.requirements.map { |r| r[:source] }.compact.first
44
+
45
+ url = info[:url] || info.fetch("url")
46
+ Source.from_url(url)
47
+ end
48
+
49
+ def crates_listing
50
+ return @crates_listing unless @crates_listing.nil?
51
+
52
+ response = Excon.get(
53
+ "https://crates.io/api/v1/crates/#{dependency.name}",
54
+ idempotent: true,
55
+ **SharedHelpers.excon_defaults
56
+ )
57
+
58
+ @crates_listing = JSON.parse(response.body)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ ################################################################################
4
+ # For more details on rust version constraints, see: #
5
+ # - https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html #
6
+ # - https://steveklabnik.github.io/semver/semver/index.html #
7
+ ################################################################################
8
+
9
+ require "dependabot/utils"
10
+ require "dependabot/cargo/version"
11
+
12
+ module Dependabot
13
+ module Cargo
14
+ class Requirement < Gem::Requirement
15
+ quoted = OPS.keys.map { |k| Regexp.quote(k) }.join("|")
16
+ version_pattern = Cargo::Version::VERSION_PATTERN
17
+
18
+ PATTERN_RAW = "\\s*(#{quoted})?\\s*(#{version_pattern})\\s*"
19
+ PATTERN = /\A#{PATTERN_RAW}\z/.freeze
20
+
21
+ # Use Cargo::Version rather than Gem::Version to ensure that
22
+ # pre-release versions aren't transformed.
23
+ def self.parse(obj)
24
+ return ["=", Cargo::Version.new(obj.to_s)] if obj.is_a?(Gem::Version)
25
+
26
+ unless (matches = PATTERN.match(obj.to_s))
27
+ msg = "Illformed requirement [#{obj.inspect}]"
28
+ raise BadRequirementError, msg
29
+ end
30
+
31
+ return DefaultRequirement if matches[1] == ">=" && matches[2] == "0"
32
+
33
+ [matches[1] || "=", Cargo::Version.new(matches[2])]
34
+ end
35
+
36
+ # For consistency with other langauges, we define a requirements array.
37
+ # Rust doesn't have an `OR` separator for requirements, so it always
38
+ # contains a single element.
39
+ def self.requirements_array(requirement_string)
40
+ [new(requirement_string)]
41
+ end
42
+
43
+ def initialize(*requirements)
44
+ requirements = requirements.flatten.flat_map do |req_string|
45
+ req_string.split(",").map do |r|
46
+ convert_rust_constraint_to_ruby_constraint(r.strip)
47
+ end
48
+ end
49
+
50
+ super(requirements)
51
+ end
52
+
53
+ private
54
+
55
+ def convert_rust_constraint_to_ruby_constraint(req_string)
56
+ req_string = req_string
57
+
58
+ if req_string.include?("*")
59
+ ruby_range(req_string.gsub(/(?:\.|^)[*]/, "").gsub(/^[^\d]/, ""))
60
+ elsif req_string.match?(/^~[^>]/) then convert_tilde_req(req_string)
61
+ elsif req_string.match?(/^[\d^]/) then convert_caret_req(req_string)
62
+ elsif req_string.match?(/[<=>]/) then req_string
63
+ else ruby_range(req_string)
64
+ end
65
+ end
66
+
67
+ def convert_tilde_req(req_string)
68
+ version = req_string.gsub(/^~/, "")
69
+ parts = version.split(".")
70
+ parts << "0" if parts.count < 3
71
+ "~> #{parts.join('.')}"
72
+ end
73
+
74
+ def ruby_range(req_string)
75
+ parts = req_string.split(".")
76
+
77
+ # If we have three or more parts then this is an exact match
78
+ return req_string if parts.count >= 3
79
+
80
+ # If we have no parts then the version is completely unlocked
81
+ return ">= 0" if parts.count.zero?
82
+
83
+ # If we have fewer than three parts we do a partial match
84
+ parts << "0"
85
+ "~> #{parts.join('.')}"
86
+ end
87
+
88
+ def convert_caret_req(req_string)
89
+ version = req_string.gsub(/^\^/, "")
90
+ parts = version.split(".")
91
+ first_non_zero = parts.find { |d| d != "0" }
92
+ first_non_zero_index =
93
+ first_non_zero ? parts.index(first_non_zero) : parts.count - 1
94
+ upper_bound = parts.map.with_index do |part, i|
95
+ if i < first_non_zero_index then part
96
+ elsif i == first_non_zero_index then (part.to_i + 1).to_s
97
+ else 0
98
+ end
99
+ end.join(".")
100
+
101
+ [">= #{version}", "< #{upper_bound}"]
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ Dependabot::Utils.
108
+ register_requirement_class("cargo", Dependabot::Cargo::Requirement)
@@ -0,0 +1,283 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "excon"
4
+ require "dependabot/git_commit_checker"
5
+ require "dependabot/update_checkers"
6
+ require "dependabot/update_checkers/base"
7
+
8
+ module Dependabot
9
+ module Cargo
10
+ class UpdateChecker < Dependabot::UpdateCheckers::Base
11
+ require_relative "update_checker/requirements_updater"
12
+ require_relative "update_checker/version_resolver"
13
+ require_relative "update_checker/file_preparer"
14
+
15
+ def latest_version
16
+ return if path_dependency?
17
+
18
+ @latest_version =
19
+ if git_dependency?
20
+ latest_version_for_git_dependency
21
+ elsif git_subdependency?
22
+ # TODO: Dependabot can't update git sub-dependencies yet, because
23
+ # they can't be passed to GitCommitChecker.
24
+ nil
25
+ else
26
+ versions = available_versions
27
+ versions.reject!(&:prerelease?) unless wants_prerelease?
28
+ versions.reject! do |v|
29
+ ignore_reqs.any? { |r| r.satisfied_by?(v) }
30
+ end
31
+ versions.max
32
+ end
33
+ end
34
+
35
+ def latest_resolvable_version
36
+ return if path_dependency?
37
+
38
+ @latest_resolvable_version ||=
39
+ if git_dependency?
40
+ latest_resolvable_version_for_git_dependency
41
+ elsif git_subdependency?
42
+ # TODO: Dependabot can't update git sub-dependencies yet, because
43
+ # they can't be passed to GitCommitChecker.
44
+ nil
45
+ else
46
+ fetch_latest_resolvable_version(unlock_requirement: true)
47
+ end
48
+ end
49
+
50
+ def latest_resolvable_version_with_no_unlock
51
+ return if path_dependency?
52
+
53
+ @latest_resolvable_version_with_no_unlock ||=
54
+ if git_dependency?
55
+ latest_resolvable_commit_with_unchanged_git_source
56
+ else
57
+ fetch_latest_resolvable_version(unlock_requirement: false)
58
+ end
59
+ end
60
+
61
+ def updated_requirements
62
+ RequirementsUpdater.new(
63
+ requirements: dependency.requirements,
64
+ updated_source: updated_source,
65
+ latest_resolvable_version: latest_resolvable_version&.to_s,
66
+ latest_version: latest_version&.to_s,
67
+ library: library?,
68
+ update_strategy: requirement_update_strategy
69
+ ).updated_requirements
70
+ end
71
+
72
+ private
73
+
74
+ def latest_version_resolvable_with_full_unlock?
75
+ # Full unlock checks aren't implemented for Rust (yet)
76
+ false
77
+ end
78
+
79
+ def updated_dependencies_after_full_unlock
80
+ raise NotImplementedError
81
+ end
82
+
83
+ def library?
84
+ # If it has a lockfile, treat it as an application. Otherwise treat it
85
+ # as a library.
86
+ dependency_files.none? { |f| f.name == "Cargo.lock" }
87
+ end
88
+
89
+ def requirement_update_strategy
90
+ library? ? :bump_versions_if_necessary : :bump_versions
91
+ end
92
+
93
+ def latest_version_for_git_dependency
94
+ latest_git_version_sha
95
+ end
96
+
97
+ def latest_git_version_sha
98
+ # If the gem isn't pinned, the latest version is just the latest
99
+ # commit for the specified branch.
100
+ unless git_commit_checker.pinned?
101
+ return git_commit_checker.head_commit_for_current_branch
102
+ end
103
+
104
+ # If the dependency is pinned to a tag that looks like a version then
105
+ # we want to update that tag. The latest version will then be the SHA
106
+ # of the latest tag that looks like a version.
107
+ if git_commit_checker.pinned_ref_looks_like_version?
108
+ latest_tag = git_commit_checker.local_tag_for_latest_version
109
+ return latest_tag&.fetch(:commit_sha) || dependency.version
110
+ end
111
+
112
+ # If the dependency is pinned to a tag that doesn't look like a
113
+ # version then there's nothing we can do.
114
+ dependency.version
115
+ end
116
+
117
+ def latest_resolvable_version_for_git_dependency
118
+ # If the gem isn't pinned, the latest version is just the latest
119
+ # commit for the specified branch.
120
+ unless git_commit_checker.pinned?
121
+ return latest_resolvable_commit_with_unchanged_git_source
122
+ end
123
+
124
+ # If the dependency is pinned to a tag that looks like a version then
125
+ # we want to update that tag. The latest version will then be the SHA
126
+ # of the latest tag that looks like a version.
127
+ if git_commit_checker.pinned_ref_looks_like_version? &&
128
+ latest_git_tag_is_resolvable?
129
+ new_tag = git_commit_checker.local_tag_for_latest_version
130
+ return new_tag.fetch(:commit_sha)
131
+ end
132
+
133
+ # If the dependency is pinned then there's nothing we can do.
134
+ dependency.version
135
+ end
136
+
137
+ def latest_git_tag_is_resolvable?
138
+ return @git_tag_resolvable if @latest_git_tag_is_resolvable_checked
139
+
140
+ @latest_git_tag_is_resolvable_checked = true
141
+
142
+ return false if git_commit_checker.local_tag_for_latest_version.nil?
143
+
144
+ replacement_tag = git_commit_checker.local_tag_for_latest_version
145
+
146
+ prepared_files = FilePreparer.new(
147
+ dependency_files: dependency_files,
148
+ dependency: dependency,
149
+ unlock_requirement: true,
150
+ replacement_git_pin: replacement_tag.fetch(:tag)
151
+ ).prepared_dependency_files
152
+
153
+ VersionResolver.new(
154
+ dependency: dependency,
155
+ prepared_dependency_files: prepared_files,
156
+ original_dependency_files: dependency_files,
157
+ credentials: credentials
158
+ ).latest_resolvable_version
159
+ @git_tag_resolvable = true
160
+ rescue SharedHelpers::HelperSubprocessFailed => error
161
+ raise error unless error.message.include?("versions conflict")
162
+
163
+ @git_tag_resolvable = false
164
+ end
165
+
166
+ def latest_resolvable_commit_with_unchanged_git_source
167
+ fetch_latest_resolvable_version(unlock_requirement: false)
168
+ rescue SharedHelpers::HelperSubprocessFailed => error
169
+ # Resolution may fail, as Cargo updates straight to the tip of the
170
+ # branch. Just return `nil` if it does (so no update).
171
+ return if error.message.include?("versions conflict")
172
+
173
+ raise error
174
+ end
175
+
176
+ def fetch_latest_resolvable_version(unlock_requirement:)
177
+ prepared_files = FilePreparer.new(
178
+ dependency_files: dependency_files,
179
+ dependency: dependency,
180
+ unlock_requirement: unlock_requirement,
181
+ latest_allowable_version: latest_version
182
+ ).prepared_dependency_files
183
+
184
+ VersionResolver.new(
185
+ dependency: dependency,
186
+ prepared_dependency_files: prepared_files,
187
+ original_dependency_files: dependency_files,
188
+ credentials: credentials
189
+ ).latest_resolvable_version
190
+ end
191
+
192
+ def updated_source
193
+ # Never need to update source, unless a git_dependency
194
+ return dependency_source_details unless git_dependency?
195
+
196
+ # Update the git tag if updating a pinned version
197
+ if git_commit_checker.pinned_ref_looks_like_version? &&
198
+ latest_git_tag_is_resolvable?
199
+ new_tag = git_commit_checker.local_tag_for_latest_version
200
+ return dependency_source_details.merge(ref: new_tag.fetch(:tag))
201
+ end
202
+
203
+ # Otherwise return the original source
204
+ dependency_source_details
205
+ end
206
+
207
+ def dependency_source_details
208
+ sources =
209
+ dependency.requirements.map { |r| r.fetch(:source) }.uniq.compact
210
+
211
+ raise "Multiple sources! #{sources.join(', ')}" if sources.count > 1
212
+
213
+ sources.first
214
+ end
215
+
216
+ def wants_prerelease?
217
+ if dependency.version &&
218
+ version_class.new(dependency.version).prerelease?
219
+ return true
220
+ end
221
+
222
+ dependency.requirements.any? do |req|
223
+ reqs = (req.fetch(:requirement) || "").split(",").map(&:strip)
224
+ reqs.any? { |r| r.match?(/[A-Za-z]/) }
225
+ end
226
+ end
227
+
228
+ def available_versions
229
+ crates_listing.
230
+ fetch("versions", []).
231
+ reject { |v| v["yanked"] }.
232
+ map { |v| version_class.new(v.fetch("num")) }
233
+ end
234
+
235
+ def git_dependency?
236
+ git_commit_checker.git_dependency?
237
+ end
238
+
239
+ def git_subdependency?
240
+ return false if dependency.top_level?
241
+
242
+ !version_class.correct?(dependency.version)
243
+ end
244
+
245
+ def path_dependency?
246
+ sources = dependency.requirements.
247
+ map { |r| r.fetch(:source) }.uniq.compact
248
+
249
+ raise "Multiple sources! #{sources.join(', ')}" if sources.count > 1
250
+
251
+ sources.first&.fetch(:type) == "path"
252
+ end
253
+
254
+ def git_commit_checker
255
+ @git_commit_checker ||=
256
+ GitCommitChecker.new(
257
+ dependency: dependency,
258
+ credentials: credentials
259
+ )
260
+ end
261
+
262
+ def crates_listing
263
+ return @crates_listing unless @crates_listing.nil?
264
+
265
+ response = Excon.get(
266
+ "https://crates.io/api/v1/crates/#{dependency.name}",
267
+ idempotent: true,
268
+ **SharedHelpers.excon_defaults
269
+ )
270
+
271
+ @crates_listing = JSON.parse(response.body)
272
+ rescue Excon::Error::Timeout
273
+ retrying ||= false
274
+ raise if retrying
275
+
276
+ retrying = true
277
+ sleep(rand(1.0..5.0)) && retry
278
+ end
279
+ end
280
+ end
281
+ end
282
+
283
+ Dependabot::UpdateCheckers.register("cargo", Dependabot::Cargo::UpdateChecker)