dependabot-dep 0.90.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "excon"
4
+ require "nokogiri"
5
+
6
+ require "dependabot/shared_helpers"
7
+ require "dependabot/source"
8
+ require "dependabot/dep/native_helpers"
9
+
10
+ module Dependabot
11
+ module Dep
12
+ module PathConverter
13
+ def self.git_url_for_path(path)
14
+ # Save a query by manually converting golang.org/x names
15
+ import_path = path.gsub(%r{^golang\.org/x}, "github.com/golang")
16
+
17
+ SharedHelpers.run_helper_subprocess(
18
+ command: NativeHelpers.helper_path,
19
+ function: "getVcsRemoteForImport",
20
+ args: { import: import_path }
21
+ )
22
+ end
23
+
24
+ # Used in dependabot-backend, which doesn't have access to any Go
25
+ # helpers.
26
+ # TODO: remove the need for this.
27
+ def self.git_url_for_path_without_go_helper(path)
28
+ # Save a query by manually converting golang.org/x names
29
+ tmp_path = path.gsub(%r{^golang\.org/x}, "github.com/golang")
30
+
31
+ # Currently, Dependabot::Source.new will return `nil` if it can't
32
+ # find a git SCH associated with a path. If it is ever extended to
33
+ # handle non-git sources we'll need to add an additional check here.
34
+ return Source.from_url(tmp_path).url if Source.from_url(tmp_path)
35
+ return "https://#{tmp_path}" if tmp_path.end_with?(".git")
36
+ return unless (metadata_response = fetch_path_metadata(path))
37
+
38
+ # Look for a GitHub, Bitbucket or GitLab URL in the response
39
+ metadata_response.scan(Dependabot::Source::SOURCE_REGEX) do
40
+ source_url = Regexp.last_match.to_s
41
+ return Source.from_url(source_url).url
42
+ end
43
+
44
+ # If none are found, parse the response and return the go-import path
45
+ doc = Nokogiri::XML(metadata_response)
46
+ doc.remove_namespaces!
47
+ import_details =
48
+ doc.xpath("//meta").
49
+ find { |n| n.attributes["name"]&.value == "go-import" }&.
50
+ attributes&.fetch("content")&.value&.split(/\s+/)
51
+ return unless import_details && import_details[1] == "git"
52
+
53
+ import_details[2]
54
+ end
55
+
56
+ def self.fetch_path_metadata(path)
57
+ # TODO: This is not robust! Instead, we should shell out to Go and
58
+ # use https://github.com/Masterminds/vcs.
59
+ response = Excon.get(
60
+ "https://#{path}?go-get=1",
61
+ idempotent: true,
62
+ **SharedHelpers.excon_defaults
63
+ )
64
+
65
+ return unless response.status == 200
66
+
67
+ response.body
68
+ end
69
+ private_class_method :fetch_path_metadata
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ ################################################################################
4
+ # For more details on Go version constraints, see: #
5
+ # - https://github.com/Masterminds/semver #
6
+ # - https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md #
7
+ ################################################################################
8
+
9
+ require "dependabot/dep/version"
10
+ require "dependabot/utils"
11
+
12
+ module Dependabot
13
+ module Dep
14
+ class Requirement < Gem::Requirement
15
+ WILDCARD_REGEX = /(?:\.|^)[xX*]/.freeze
16
+ OR_SEPARATOR = /(?<=[a-zA-Z0-9*])\s*\|{2}/.freeze
17
+
18
+ # Override the version pattern to allow a 'v' prefix
19
+ quoted = OPS.keys.map { |k| Regexp.quote(k) }.join("|")
20
+ version_pattern = "v?#{Dep::Version::VERSION_PATTERN}"
21
+
22
+ PATTERN_RAW = "\\s*(#{quoted})?\\s*(#{version_pattern})\\s*"
23
+ PATTERN = /\A#{PATTERN_RAW}\z/.freeze
24
+
25
+ # Use Dep::Version rather than Gem::Version to ensure that
26
+ # pre-release versions aren't transformed.
27
+ def self.parse(obj)
28
+ return ["=", Dep::Version.new(obj.to_s)] if obj.is_a?(Gem::Version)
29
+
30
+ unless (matches = PATTERN.match(obj.to_s))
31
+ msg = "Illformed requirement [#{obj.inspect}]"
32
+ raise BadRequirementError, msg
33
+ end
34
+
35
+ return DefaultRequirement if matches[1] == ">=" && matches[2] == "0"
36
+
37
+ [matches[1] || "=", Dep::Version.new(matches[2])]
38
+ end
39
+
40
+ # Returns an array of requirements. At least one requirement from the
41
+ # returned array must be satisfied for a version to be valid.
42
+ def self.requirements_array(requirement_string)
43
+ return [new(nil)] if requirement_string.nil?
44
+
45
+ requirement_string.strip.split(OR_SEPARATOR).map do |req_string|
46
+ new(req_string)
47
+ end
48
+ end
49
+
50
+ def initialize(*requirements)
51
+ requirements = requirements.flatten.flat_map do |req_string|
52
+ req_string.split(",").map do |r|
53
+ convert_go_constraint_to_ruby_constraint(r.strip)
54
+ end
55
+ end
56
+
57
+ super(requirements)
58
+ end
59
+
60
+ private
61
+
62
+ def convert_go_constraint_to_ruby_constraint(req_string)
63
+ req_string = req_string
64
+ req_string = convert_wildcard_characters(req_string)
65
+
66
+ if req_string.match?(WILDCARD_REGEX)
67
+ ruby_range(req_string.gsub(WILDCARD_REGEX, "").gsub(/^[^\d]/, ""))
68
+ elsif req_string.match?(/^~[^>]/) then convert_tilde_req(req_string)
69
+ elsif req_string.include?(" - ") then convert_hyphen_req(req_string)
70
+ elsif req_string.match?(/^[\dv^]/) then convert_caret_req(req_string)
71
+ elsif req_string.match?(/[<=>]/) then req_string
72
+ else ruby_range(req_string)
73
+ end
74
+ end
75
+
76
+ def convert_wildcard_characters(req_string)
77
+ if req_string.match?(/^[\dv^>~]/)
78
+ replace_wildcard_in_lower_bound(req_string)
79
+ elsif req_string.start_with?("<")
80
+ parts = req_string.split(".")
81
+ parts.map.with_index do |part, index|
82
+ next "0" if part.match?(WILDCARD_REGEX)
83
+ next part.to_i + 1 if parts[index + 1]&.match?(WILDCARD_REGEX)
84
+
85
+ part
86
+ end.join(".")
87
+ else
88
+ req_string
89
+ end
90
+ end
91
+
92
+ def replace_wildcard_in_lower_bound(req_string)
93
+ after_wildcard = false
94
+
95
+ if req_string.start_with?("~")
96
+ req_string = req_string.gsub(/(?:(?:\.|^)[xX*])(\.[xX*])+/, "")
97
+ end
98
+
99
+ req_string.split(".").
100
+ map do |part|
101
+ part.split("-").map.with_index do |p, i|
102
+ # Before we hit a wildcard we just return the existing part
103
+ next p unless p.match?(WILDCARD_REGEX) || after_wildcard
104
+
105
+ # On or after a wildcard we replace the version part with zero
106
+ after_wildcard = true
107
+ i.zero? ? "0" : "a"
108
+ end.join("-")
109
+ end.join(".")
110
+ end
111
+
112
+ def convert_tilde_req(req_string)
113
+ version = req_string.gsub(/^~/, "")
114
+ parts = version.split(".")
115
+ parts << "0" if parts.count < 3
116
+ "~> #{parts.join('.')}"
117
+ end
118
+
119
+ def convert_hyphen_req(req_string)
120
+ lower_bound, upper_bound = req_string.split(/\s+-\s+/)
121
+ [">= #{lower_bound}", "<= #{upper_bound}"]
122
+ end
123
+
124
+ def ruby_range(req_string)
125
+ parts = req_string.split(".")
126
+
127
+ # If we have three or more parts then this is an exact match
128
+ return req_string if parts.count >= 3
129
+
130
+ # If we have no parts then the version is completely unlocked
131
+ return ">= 0" if parts.count.zero?
132
+
133
+ # If we have fewer than three parts we do a partial match
134
+ parts << "0"
135
+ "~> #{parts.join('.')}"
136
+ end
137
+
138
+ # Note: Dep's caret notation implementation doesn't distinguish between
139
+ # pre and post-1.0.0 requirements (unlike in JS)
140
+ def convert_caret_req(req_string)
141
+ version = req_string.gsub(/^\^?v?/, "")
142
+ parts = version.split(".")
143
+ upper_bound = [parts.first.to_i + 1, 0, 0, "a"].map(&:to_s).join(".")
144
+
145
+ [">= #{version}", "< #{upper_bound}"]
146
+ end
147
+ end
148
+ end
149
+ end
150
+
151
+ Dependabot::Utils.
152
+ register_requirement_class("dep", Dependabot::Dep::Requirement)
@@ -0,0 +1,312 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "toml-rb"
4
+ require "dependabot/update_checkers"
5
+ require "dependabot/update_checkers/base"
6
+
7
+ module Dependabot
8
+ module Dep
9
+ class UpdateChecker < Dependabot::UpdateCheckers::Base
10
+ require_relative "update_checker/file_preparer"
11
+ require_relative "update_checker/latest_version_finder"
12
+ require_relative "update_checker/requirements_updater"
13
+ require_relative "update_checker/version_resolver"
14
+
15
+ def latest_version
16
+ @latest_version ||=
17
+ LatestVersionFinder.new(
18
+ dependency: dependency,
19
+ dependency_files: dependency_files,
20
+ credentials: credentials,
21
+ ignored_versions: ignored_versions
22
+ ).latest_version
23
+ end
24
+
25
+ def latest_resolvable_version
26
+ @latest_resolvable_version ||=
27
+ if modules_dependency?
28
+ latest_version
29
+ elsif git_dependency?
30
+ latest_resolvable_version_for_git_dependency
31
+ else
32
+ latest_resolvable_released_version(unlock_requirement: true)
33
+ end
34
+ end
35
+
36
+ def latest_resolvable_version_with_no_unlock
37
+ @latest_resolvable_version_with_no_unlock ||=
38
+ if git_dependency?
39
+ latest_resolvable_commit_with_unchanged_git_source
40
+ else
41
+ latest_resolvable_released_version(unlock_requirement: false)
42
+ end
43
+ end
44
+
45
+ def updated_requirements
46
+ @updated_requirements ||=
47
+ RequirementsUpdater.new(
48
+ requirements: dependency.requirements,
49
+ updated_source: updated_source,
50
+ update_strategy: requirements_update_strategy,
51
+ latest_version: latest_version&.to_s,
52
+ latest_resolvable_version: latest_resolvable_version&.to_s
53
+ ).updated_requirements
54
+ end
55
+
56
+ def requirements_update_strategy
57
+ # If passed in as an option (in the base class) honour that option
58
+ if @requirements_update_strategy
59
+ return @requirements_update_strategy.to_sym
60
+ end
61
+
62
+ # Otherwise, widen ranges for libraries and bump versions for apps
63
+ library? ? :widen_ranges : :bump_versions
64
+ end
65
+
66
+ private
67
+
68
+ def latest_version_resolvable_with_full_unlock?
69
+ # Full unlock checks aren't implemented for Go (yet)
70
+ false
71
+ end
72
+
73
+ def updated_dependencies_after_full_unlock
74
+ raise NotImplementedError
75
+ end
76
+
77
+ # Override the base class's check for whether this is a git dependency,
78
+ # since not all dep git dependencies have a SHA version (sometimes their
79
+ # version is the tag)
80
+ def existing_version_is_sha?
81
+ git_dependency?
82
+ end
83
+
84
+ def library?
85
+ dependency_files.none? { |f| f.type == "package_main" }
86
+ end
87
+
88
+ # rubocop:disable Metrics/CyclomaticComplexity
89
+ # rubocop:disable Metrics/PerceivedComplexity
90
+ def latest_resolvable_version_for_git_dependency
91
+ return latest_version if modules_dependency?
92
+
93
+ latest_release =
94
+ begin
95
+ latest_resolvable_released_version(unlock_requirement: true)
96
+ rescue SharedHelpers::HelperSubprocessFailed => error
97
+ raise unless error.message.include?("Solving failure")
98
+ end
99
+
100
+ # If there's a resolvable release that includes the current pinned
101
+ # ref or that the current branch is behind, we switch to that release.
102
+ return latest_release if git_branch_or_ref_in_release?(latest_release)
103
+
104
+ # Otherwise, if the gem isn't pinned, the latest version is just the
105
+ # latest commit for the specified branch.
106
+ unless git_commit_checker.pinned?
107
+ return latest_resolvable_commit_with_unchanged_git_source
108
+ end
109
+
110
+ # If the dependency is pinned to a tag that looks like a version then
111
+ # we want to update that tag.
112
+ if git_commit_checker.pinned_ref_looks_like_version? &&
113
+ latest_git_tag_is_resolvable?
114
+ new_tag = git_commit_checker.local_tag_for_latest_version
115
+ return version_from_tag(new_tag)
116
+ end
117
+
118
+ # If the dependency is pinned to a tag that doesn't look like a
119
+ # version then there's nothing we can do.
120
+ nil
121
+ end
122
+ # rubocop:enable Metrics/CyclomaticComplexity
123
+ # rubocop:enable Metrics/PerceivedComplexity
124
+
125
+ def version_from_tag(tag)
126
+ # To compare with the current version we either use the commit SHA
127
+ # (if that's what the parser picked up) of the tag name.
128
+ if dependency.version&.match?(/^[0-9a-f]{40}$/)
129
+ return tag&.fetch(:commit_sha)
130
+ end
131
+
132
+ tag&.fetch(:tag)
133
+ end
134
+
135
+ def latest_resolvable_commit_with_unchanged_git_source
136
+ if @commit_lookup_attempted
137
+ return @latest_resolvable_commit_with_unchanged_git_source
138
+ end
139
+
140
+ @commit_lookup_attempted = true
141
+ @latest_resolvable_commit_with_unchanged_git_source ||=
142
+ begin
143
+ prepared_files = FilePreparer.new(
144
+ dependency_files: dependency_files,
145
+ dependency: dependency,
146
+ unlock_requirement: false,
147
+ remove_git_source: false,
148
+ latest_allowable_version: latest_version
149
+ ).prepared_dependency_files
150
+
151
+ VersionResolver.new(
152
+ dependency: dependency,
153
+ dependency_files: prepared_files,
154
+ credentials: credentials
155
+ ).latest_resolvable_version
156
+ end
157
+ rescue SharedHelpers::HelperSubprocessFailed => error
158
+ # This should rescue resolvability errors in future
159
+ raise unless error.message.include?("Solving failure")
160
+ end
161
+
162
+ def latest_resolvable_released_version(unlock_requirement:)
163
+ @latest_resolvable_released_version ||= {}
164
+ @latest_resolvable_released_version[unlock_requirement] ||=
165
+ begin
166
+ prepared_files = FilePreparer.new(
167
+ dependency_files: dependency_files,
168
+ dependency: dependency,
169
+ unlock_requirement: unlock_requirement,
170
+ remove_git_source: git_dependency?,
171
+ latest_allowable_version: latest_version
172
+ ).prepared_dependency_files
173
+
174
+ VersionResolver.new(
175
+ dependency: dependency,
176
+ dependency_files: prepared_files,
177
+ credentials: credentials
178
+ ).latest_resolvable_version
179
+ end
180
+ end
181
+
182
+ def latest_git_tag_is_resolvable?
183
+ return @git_tag_resolvable if @latest_git_tag_is_resolvable_checked
184
+
185
+ @latest_git_tag_is_resolvable_checked = true
186
+
187
+ return false if git_commit_checker.local_tag_for_latest_version.nil?
188
+
189
+ replacement_tag = git_commit_checker.local_tag_for_latest_version
190
+
191
+ prepared_files = FilePreparer.new(
192
+ dependency: dependency,
193
+ dependency_files: dependency_files,
194
+ unlock_requirement: false,
195
+ remove_git_source: false,
196
+ replacement_git_pin: replacement_tag.fetch(:tag)
197
+ ).prepared_dependency_files
198
+
199
+ VersionResolver.new(
200
+ dependency: dependency,
201
+ dependency_files: prepared_files,
202
+ credentials: credentials
203
+ ).latest_resolvable_version
204
+
205
+ @git_tag_resolvable = true
206
+ rescue SharedHelpers::HelperSubprocessFailed => error
207
+ # This should rescue resolvability errors in future
208
+ raise unless error.message.include?("Solving failure")
209
+
210
+ @git_tag_resolvable = false
211
+ end
212
+
213
+ def updated_source
214
+ # Never need to update source, unless a git_dependency
215
+ return dependency_source_details unless git_dependency?
216
+
217
+ # Source becomes `nil` if switching to default rubygems
218
+ return default_source if should_switch_source_from_ref_to_release?
219
+
220
+ # Update the git tag if updating a pinned version
221
+ if git_commit_checker.pinned_ref_looks_like_version? &&
222
+ latest_git_tag_is_resolvable?
223
+ new_tag = git_commit_checker.local_tag_for_latest_version
224
+ return dependency_source_details.merge(ref: new_tag.fetch(:tag))
225
+ end
226
+
227
+ # Otherwise return the original source
228
+ dependency_source_details
229
+ end
230
+
231
+ def dependency_source_details
232
+ sources =
233
+ dependency.requirements.map { |r| r.fetch(:source) }.uniq.compact
234
+
235
+ raise "Multiple sources! #{sources.join(', ')}" if sources.count > 1
236
+
237
+ sources.first
238
+ end
239
+
240
+ def should_switch_source_from_ref_to_release?
241
+ return false unless git_dependency?
242
+ return false if latest_resolvable_version_for_git_dependency.nil?
243
+
244
+ Gem::Version.correct?(latest_resolvable_version_for_git_dependency)
245
+ end
246
+
247
+ def modules_dependency?
248
+ # If dep is being used then we use that to determine the latest
249
+ # version we can update to (since it will have resolvability
250
+ # requirements, whereas Go modules won't)
251
+ !dependency_in_gopkg_lock?
252
+ end
253
+
254
+ def dependency_in_gopkg_lock?
255
+ lockfile = dependency_files.find { |f| f.name == "Gopkg.lock" }
256
+ return false unless lockfile
257
+
258
+ parsed_file(lockfile).fetch("projects", []).any? do |details|
259
+ details.fetch("name") == dependency.name
260
+ end
261
+ end
262
+
263
+ def git_dependency?
264
+ git_commit_checker.git_dependency?
265
+ end
266
+
267
+ def default_source
268
+ if modules_dependency?
269
+ return { type: "default", source: dependency.name }
270
+ end
271
+
272
+ original_declaration =
273
+ parsed_file(manifest).
274
+ values_at(*Dep::FileParser::REQUIREMENT_TYPES).
275
+ flatten.compact.
276
+ find { |d| d["name"] == dependency.name }
277
+
278
+ {
279
+ type: "default",
280
+ source:
281
+ original_declaration&.fetch("source", nil) || dependency.name
282
+ }
283
+ end
284
+
285
+ def git_branch_or_ref_in_release?(release)
286
+ return false unless release
287
+
288
+ git_commit_checker.branch_or_ref_in_release?(release)
289
+ end
290
+
291
+ def parsed_file(file)
292
+ @parsed_file ||= {}
293
+ @parsed_file[file.name] ||= TomlRB.parse(file.content)
294
+ end
295
+
296
+ def manifest
297
+ @manifest ||= dependency_files.find { |f| f.name == "Gopkg.toml" }
298
+ end
299
+
300
+ def git_commit_checker
301
+ @git_commit_checker ||=
302
+ GitCommitChecker.new(
303
+ dependency: dependency,
304
+ credentials: credentials,
305
+ ignored_versions: ignored_versions
306
+ )
307
+ end
308
+ end
309
+ end
310
+ end
311
+
312
+ Dependabot::UpdateCheckers.register("dep", Dependabot::Dep::UpdateChecker)