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.
- checksums.yaml +7 -0
- data/lib/dependabot/cargo.rb +11 -0
- data/lib/dependabot/cargo/file_fetcher.rb +241 -0
- data/lib/dependabot/cargo/file_parser.rb +214 -0
- data/lib/dependabot/cargo/file_updater.rb +83 -0
- data/lib/dependabot/cargo/file_updater/lockfile_updater.rb +247 -0
- data/lib/dependabot/cargo/file_updater/manifest_updater.rb +158 -0
- data/lib/dependabot/cargo/metadata_finder.rb +62 -0
- data/lib/dependabot/cargo/requirement.rb +108 -0
- data/lib/dependabot/cargo/update_checker.rb +283 -0
- data/lib/dependabot/cargo/update_checker/file_preparer.rb +200 -0
- data/lib/dependabot/cargo/update_checker/requirements_updater.rb +173 -0
- data/lib/dependabot/cargo/update_checker/version_resolver.rb +239 -0
- data/lib/dependabot/cargo/version.rb +34 -0
- metadata +183 -0
|
@@ -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)
|