dependabot-dep 0.90.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,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)