dependabot-common 0.236.0 → 0.238.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/lib/dependabot/clients/azure.rb +3 -3
  3. data/lib/dependabot/clients/codecommit.rb +1 -0
  4. data/lib/dependabot/config/file.rb +17 -6
  5. data/lib/dependabot/config/update_config.rb +23 -5
  6. data/lib/dependabot/dependency.rb +137 -27
  7. data/lib/dependabot/dependency_file.rb +84 -14
  8. data/lib/dependabot/dependency_group.rb +29 -5
  9. data/lib/dependabot/errors.rb +335 -13
  10. data/lib/dependabot/file_fetchers/base.rb +227 -93
  11. data/lib/dependabot/file_updaters/base.rb +1 -1
  12. data/lib/dependabot/git_commit_checker.rb +6 -0
  13. data/lib/dependabot/git_metadata_fetcher.rb +58 -20
  14. data/lib/dependabot/git_ref.rb +71 -0
  15. data/lib/dependabot/metadata_finders/base/changelog_finder.rb +13 -6
  16. data/lib/dependabot/pull_request_creator/github.rb +11 -8
  17. data/lib/dependabot/pull_request_creator/message.rb +21 -2
  18. data/lib/dependabot/pull_request_creator/message_builder/link_and_mention_sanitizer.rb +37 -16
  19. data/lib/dependabot/pull_request_creator/message_builder/metadata_presenter.rb +4 -2
  20. data/lib/dependabot/pull_request_creator/message_builder.rb +54 -4
  21. data/lib/dependabot/pull_request_creator/pr_name_prefixer.rb +10 -4
  22. data/lib/dependabot/shared_helpers.rb +117 -33
  23. data/lib/dependabot/simple_instrumentor.rb +22 -3
  24. data/lib/dependabot/source.rb +65 -17
  25. data/lib/dependabot/update_checkers/version_filters.rb +12 -1
  26. data/lib/dependabot/utils.rb +21 -2
  27. data/lib/dependabot/workspace/base.rb +42 -7
  28. data/lib/dependabot/workspace/change_attempt.rb +31 -3
  29. data/lib/dependabot/workspace/git.rb +34 -4
  30. data/lib/dependabot/workspace.rb +16 -2
  31. data/lib/dependabot.rb +1 -1
  32. metadata +38 -9
@@ -85,7 +85,7 @@ module Dependabot
85
85
  msg = (msg[0..trunc_length] + tr_msg)
86
86
  end
87
87
  # if we used a custom encoding for calculating length, then we need to force back to UTF-8
88
- msg.force_encoding(Encoding::UTF_8) unless pr_message_encoding.nil?
88
+ msg = msg.encode("utf-8", "binary", invalid: :replace, undef: :replace) unless pr_message_encoding.nil?
89
89
  msg
90
90
  end
91
91
 
@@ -162,7 +162,13 @@ module Dependabot
162
162
 
163
163
  def group_pr_name
164
164
  updates = dependencies.map(&:name).uniq.count
165
- "bump the #{dependency_group.name} group#{pr_name_directory} with #{updates} update#{'s' if updates > 1}"
165
+
166
+ if source&.directories
167
+ "bump the #{dependency_group.name} across #{source.directories.count} directories " \
168
+ "with #{updates} update#{'s' if updates > 1}"
169
+ else
170
+ "bump the #{dependency_group.name} group#{pr_name_directory} with #{updates} update#{'s' if updates > 1}"
171
+ end
166
172
  end
167
173
 
168
174
  def pr_name_prefix
@@ -260,6 +266,8 @@ module Dependabot
260
266
  # rubocop:disable Metrics/PerceivedComplexity
261
267
  # rubocop:disable Metrics/AbcSize
262
268
  def version_commit_message_intro
269
+ return multi_directory_group_intro if dependency_group && source&.directories
270
+
263
271
  return group_intro if dependency_group
264
272
 
265
273
  return multidependency_property_intro if dependencies.count > 1 && updating_a_property?
@@ -346,6 +354,42 @@ module Dependabot
346
354
  msg
347
355
  end
348
356
 
357
+ def multi_directory_group_intro
358
+ msg = ""
359
+
360
+ source.directories.each do |directory|
361
+ dependencies_in_directory = dependencies.select { |dep| dep.metadata[:directory] == directory }
362
+ next unless dependencies_in_directory.any?
363
+
364
+ update_count = dependencies_in_directory.map(&:name).uniq.count
365
+
366
+ msg += "Bumps the #{dependency_group.name} " \
367
+ "with #{update_count} update#{update_count > 1 ? 's' : ''} in the #{directory} directory:"
368
+
369
+ msg += if update_count >= 5
370
+ header = %w(Package From To)
371
+ rows = dependencies_in_directory.map do |dep|
372
+ [
373
+ dependency_link(dep),
374
+ "`#{dep.humanized_previous_version}`",
375
+ "`#{dep.humanized_version}`"
376
+ ]
377
+ end
378
+ "\n\n#{table([header] + rows)}"
379
+ elsif update_count > 1
380
+ dependency_links_in_directory = dependency_links_for_directory(directory)
381
+ " #{dependency_links_in_directory[0..-2].join(', ')} and #{dependency_links_in_directory[-1]}."
382
+ else
383
+ dependency_links_in_directory = dependency_links_for_directory(directory)
384
+ " #{dependency_links_in_directory.first}."
385
+ end
386
+
387
+ msg += "\n"
388
+ end
389
+
390
+ msg
391
+ end
392
+
349
393
  def group_intro
350
394
  update_count = dependencies.map(&:name).uniq.count
351
395
 
@@ -427,6 +471,12 @@ module Dependabot
427
471
  @dependency_links = uniq_deps.map { |dep| dependency_link(dep) }
428
472
  end
429
473
 
474
+ def dependency_links_for_directory(directory)
475
+ dependencies_in_directory = dependencies.select { |dep| dep.metadata[:directory] == directory }
476
+ uniq_deps = dependencies_in_directory.each_with_object({}) { |dep, memo| memo[dep.name] ||= dep }.values
477
+ @dependency_links = uniq_deps.map { |dep| dependency_link(dep) }
478
+ end
479
+
430
480
  def dependency_link(dependency)
431
481
  if source_url(dependency)
432
482
  "[#{dependency.display_name}](#{source_url(dependency)})"
@@ -483,8 +533,8 @@ module Dependabot
483
533
  "| #{row.join(' | ')} |"
484
534
  end
485
535
 
486
- def metadata_cascades
487
- return metadata_cascades_for_dep(dependencies.first) if dependencies.one?
536
+ def metadata_cascades # rubocop:disable Metrics/PerceivedComplexity
537
+ return metadata_cascades_for_dep(dependencies.first) if dependencies.one? && !dependency_group
488
538
 
489
539
  dependencies.map do |dep|
490
540
  msg = if dep.removed?
@@ -132,7 +132,7 @@ module Dependabot
132
132
  case last_dependabot_commit_style
133
133
  when :gitmoji then true
134
134
  when :conventional_prefix, :conventional_prefix_with_scope
135
- last_dependabot_commit_message.match?(/: (\[[Ss]ecurity\] )?(B|U)/)
135
+ last_dependabot_commit_title.match?(/: (\[[Ss]ecurity\] )?(B|U)/)
136
136
  else raise "Unknown commit style #{last_dependabot_commit_style}"
137
137
  end
138
138
  end
@@ -152,7 +152,7 @@ module Dependabot
152
152
  end
153
153
 
154
154
  def last_dependabot_commit_style
155
- return unless (msg = last_dependabot_commit_message)
155
+ return unless (msg = last_dependabot_commit_title)
156
156
 
157
157
  return :gitmoji if msg.start_with?("⬆️")
158
158
  return :conventional_prefix if msg.match?(/\A(chore|build|upgrade):/i)
@@ -162,7 +162,7 @@ module Dependabot
162
162
  end
163
163
 
164
164
  def last_dependabot_commit_prefix
165
- last_dependabot_commit_message&.split(/[:(]/)&.first
165
+ last_dependabot_commit_title&.split(/[:(]/)&.first
166
166
  end
167
167
 
168
168
  # rubocop:disable Metrics/PerceivedComplexity
@@ -245,7 +245,7 @@ module Dependabot
245
245
  ANGULAR_PREFIXES.any? { |pre| message.match?(/#{pre}[:(]/i) }
246
246
  end
247
247
 
248
- return last_dependabot_commit_message&.start_with?(/[A-Z]/) if semantic_messages.none?
248
+ return last_dependabot_commit_title&.start_with?(/[A-Z]/) if semantic_messages.none?
249
249
 
250
250
  capitalized_msgs = semantic_messages
251
251
  .select { |m| m.start_with?(/[A-Z]/) }
@@ -329,6 +329,12 @@ module Dependabot
329
329
  .map(&:strip)
330
330
  end
331
331
 
332
+ def last_dependabot_commit_title
333
+ return @last_dependabot_commit_title if defined?(@last_dependabot_commit_title)
334
+
335
+ @last_dependabot_commit_title = last_dependabot_commit_message&.split("\n")&.first
336
+ end
337
+
332
338
  def last_dependabot_commit_message
333
339
  @last_dependabot_commit_message ||=
334
340
  case source.provider
@@ -1,4 +1,4 @@
1
- # typed: false
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "digest"
@@ -8,6 +8,7 @@ require "fileutils"
8
8
  require "json"
9
9
  require "open3"
10
10
  require "shellwords"
11
+ require "sorbet-runtime"
11
12
  require "tmpdir"
12
13
 
13
14
  require "dependabot/simple_instrumentor"
@@ -18,20 +19,34 @@ require "dependabot"
18
19
 
19
20
  module Dependabot
20
21
  module SharedHelpers
21
- GIT_CONFIG_GLOBAL_PATH = File.expand_path(".gitconfig", Utils::BUMP_TMP_DIR_PATH)
22
- USER_AGENT = "dependabot-core/#{Dependabot::VERSION} " \
23
- "#{Excon::USER_AGENT} ruby/#{RUBY_VERSION} " \
24
- "(#{RUBY_PLATFORM}) " \
25
- "(+https://github.com/dependabot/dependabot-core)".freeze
22
+ extend T::Sig
23
+
24
+ GIT_CONFIG_GLOBAL_PATH = T.let(File.expand_path(".gitconfig", Utils::BUMP_TMP_DIR_PATH), String)
25
+ USER_AGENT = T.let(
26
+ "dependabot-core/#{Dependabot::VERSION} " \
27
+ "#{Excon::USER_AGENT} ruby/#{RUBY_VERSION} " \
28
+ "(#{RUBY_PLATFORM}) " \
29
+ "(+https://github.com/dependabot/dependabot-core)".freeze,
30
+ String
31
+ )
26
32
  SIGKILL = 9
27
33
 
34
+ sig do
35
+ type_parameters(:T)
36
+ .params(
37
+ directory: String,
38
+ repo_contents_path: T.nilable(String),
39
+ block: T.proc.params(arg0: T.any(Pathname, String)).returns(T.type_parameter(:T))
40
+ )
41
+ .returns(T.type_parameter(:T))
42
+ end
28
43
  def self.in_a_temporary_repo_directory(directory = "/", repo_contents_path = nil, &block)
29
44
  if repo_contents_path
30
45
  # If a workspace has been defined to allow orcestration of the git repo
31
46
  # by the runtime we should defer to it, otherwise we prepare the folder
32
47
  # for direct use and yield.
33
48
  if Dependabot::Workspace.active_workspace
34
- Dependabot::Workspace.active_workspace.change(&block)
49
+ T.must(Dependabot::Workspace.active_workspace).change(&block)
35
50
  else
36
51
  path = Pathname.new(File.join(repo_contents_path, directory)).expand_path
37
52
  reset_git_repo(repo_contents_path)
@@ -46,9 +61,18 @@ module Dependabot
46
61
  end
47
62
  end
48
63
 
49
- def self.in_a_temporary_directory(directory = "/")
64
+ sig do
65
+ type_parameters(:T)
66
+ .params(
67
+ directory: String,
68
+ _block: T.proc.params(arg0: T.any(Pathname, String)).returns(T.type_parameter(:T))
69
+ )
70
+ .returns(T.type_parameter(:T))
71
+ end
72
+ def self.in_a_temporary_directory(directory = "/", &_block)
50
73
  FileUtils.mkdir_p(Utils::BUMP_TMP_DIR_PATH)
51
74
  tmp_dir = Dir.mktmpdir(Utils::BUMP_TMP_FILE_PREFIX, Utils::BUMP_TMP_DIR_PATH)
75
+ path = Pathname.new(File.join(tmp_dir, directory)).expand_path
52
76
 
53
77
  begin
54
78
  path = Pathname.new(File.join(tmp_dir, directory)).expand_path
@@ -60,28 +84,59 @@ module Dependabot
60
84
  end
61
85
 
62
86
  class HelperSubprocessFailed < Dependabot::DependabotError
63
- attr_reader :error_class, :error_context, :trace
87
+ extend T::Sig
88
+
89
+ sig { returns(String) }
90
+ attr_reader :error_class
91
+
92
+ sig { returns(T::Hash[Symbol, String]) }
93
+ attr_reader :error_context
94
+
95
+ sig { returns(T.nilable(T::Array[String])) }
96
+ attr_reader :trace
64
97
 
98
+ sig do
99
+ params(
100
+ message: String,
101
+ error_context: T::Hash[Symbol, String],
102
+ error_class: T.nilable(String),
103
+ trace: T.nilable(T::Array[String])
104
+ ).void
105
+ end
65
106
  def initialize(message:, error_context:, error_class: nil, trace: nil)
66
107
  super(message)
67
- @error_class = error_class || "HelperSubprocessFailed"
108
+ @error_class = T.let(error_class || "HelperSubprocessFailed", String)
68
109
  @error_context = error_context
69
- @fingerprint = error_context[:fingerprint] || error_context[:command]
110
+ @fingerprint = T.let(error_context[:fingerprint] || error_context[:command], T.nilable(String))
70
111
  @trace = trace
71
112
  end
72
113
 
114
+ sig { returns(T::Hash[Symbol, T.untyped]) }
73
115
  def raven_context
74
116
  { fingerprint: [@fingerprint], extra: @error_context.except(:stderr_output, :fingerprint) }
75
117
  end
76
118
  end
77
119
 
78
120
  # Escapes all special characters, e.g. = & | <>
121
+ sig { params(command: String).returns(String) }
79
122
  def self.escape_command(command)
80
123
  command_parts = command.split.map(&:strip).reject(&:empty?)
81
124
  Shellwords.join(command_parts)
82
125
  end
83
126
 
84
127
  # rubocop:disable Metrics/MethodLength
128
+ # rubocop:disable Metrics/AbcSize
129
+ sig do
130
+ params(
131
+ command: String,
132
+ function: String,
133
+ args: T.any(T::Array[String], T::Hash[Symbol, String]),
134
+ env: T.nilable(T::Hash[String, String]),
135
+ stderr_to_stdout: T::Boolean,
136
+ allow_unsafe_shell_command: T::Boolean
137
+ )
138
+ .returns(T.nilable(T.any(String, T::Hash[String, T.untyped], T::Array[T::Hash[String, T.untyped]])))
139
+ end
85
140
  def self.run_helper_subprocess(command:, function:, args:, env: nil,
86
141
  stderr_to_stdout: false,
87
142
  allow_unsafe_shell_command: false)
@@ -95,11 +150,11 @@ module Dependabot
95
150
  if ENV["DEBUG_FUNCTION"] == function
96
151
  puts helper_subprocess_bash_command(stdin_data: stdin_data, command: cmd, env: env)
97
152
  # Pause execution so we can run helpers inside the temporary directory
98
- debugger # rubocop:disable Lint/Debugger
153
+ T.unsafe(self).debugger
99
154
  end
100
155
 
101
156
  env_cmd = [env, cmd].compact
102
- stdout, stderr, process = Open3.capture3(*env_cmd, stdin_data: stdin_data)
157
+ stdout, stderr, process = T.unsafe(Open3).capture3(*env_cmd, stdin_data: stdin_data)
103
158
  time_taken = Time.now - start
104
159
 
105
160
  if ENV["DEBUG_HELPERS"] == "true"
@@ -126,24 +181,27 @@ module Dependabot
126
181
 
127
182
  check_out_of_memory_error(stderr, error_context)
128
183
 
129
- response = JSON.parse(stdout)
130
- return response["result"] if process.success?
131
-
132
- raise HelperSubprocessFailed.new(
133
- message: response["error"],
134
- error_class: response["error_class"],
135
- error_context: error_context,
136
- trace: response["trace"]
137
- )
138
- rescue JSON::ParserError
139
- raise HelperSubprocessFailed.new(
140
- message: stdout || "No output from command",
141
- error_class: "JSON::ParserError",
142
- error_context: error_context
143
- )
184
+ begin
185
+ response = JSON.parse(stdout)
186
+ return response["result"] if process.success?
187
+
188
+ raise HelperSubprocessFailed.new(
189
+ message: response["error"],
190
+ error_class: response["error_class"],
191
+ error_context: error_context,
192
+ trace: response["trace"]
193
+ )
194
+ rescue JSON::ParserError
195
+ raise HelperSubprocessFailed.new(
196
+ message: stdout || "No output from command",
197
+ error_class: "JSON::ParserError",
198
+ error_context: error_context
199
+ )
200
+ end
144
201
  end
145
202
 
146
203
  # rubocop:enable Metrics/MethodLength
204
+ sig { params(stderr: T.nilable(String), error_context: T::Hash[Symbol, String]).void }
147
205
  def self.check_out_of_memory_error(stderr, error_context)
148
206
  return unless stderr&.include?("JavaScript heap out of memory")
149
207
 
@@ -154,12 +212,14 @@ module Dependabot
154
212
  )
155
213
  end
156
214
 
215
+ sig { returns(T::Array[T.class_of(Excon::Middleware::Base)]) }
157
216
  def self.excon_middleware
158
- Excon.defaults[:middlewares] +
217
+ T.must(T.cast(Excon.defaults, T::Hash[Symbol, T::Array[T.class_of(Excon::Middleware::Base)]])[:middlewares]) +
159
218
  [Excon::Middleware::Decompress] +
160
219
  [Excon::Middleware::RedirectFollower]
161
220
  end
162
221
 
222
+ sig { params(headers: T.nilable(T::Hash[String, String])).returns(T::Hash[String, String]) }
163
223
  def self.excon_headers(headers = nil)
164
224
  headers ||= {}
165
225
  {
@@ -167,9 +227,10 @@ module Dependabot
167
227
  }.merge(headers)
168
228
  end
169
229
 
230
+ sig { params(options: T.nilable(T::Hash[Symbol, T.untyped])).returns(T::Hash[Symbol, T.untyped]) }
170
231
  def self.excon_defaults(options = nil)
171
232
  options ||= {}
172
- headers = options.delete(:headers)
233
+ headers = T.cast(options.delete(:headers), T.nilable(T::Hash[String, String]))
173
234
  {
174
235
  instrumentor: Dependabot::SimpleInstrumentor,
175
236
  connect_timeout: 5,
@@ -182,7 +243,15 @@ module Dependabot
182
243
  }.merge(options)
183
244
  end
184
245
 
185
- def self.with_git_configured(credentials:)
246
+ sig do
247
+ type_parameters(:T)
248
+ .params(
249
+ credentials: T::Array[T::Hash[String, String]],
250
+ _block: T.proc.returns(T.type_parameter(:T))
251
+ )
252
+ .returns(T.type_parameter(:T))
253
+ end
254
+ def self.with_git_configured(credentials:, &_block)
186
255
  safe_directories = find_safe_directories
187
256
 
188
257
  FileUtils.mkdir_p(Utils::BUMP_TMP_DIR_PATH)
@@ -203,18 +272,20 @@ module Dependabot
203
272
  end
204
273
 
205
274
  # Handle SCP-style git URIs
275
+ sig { params(uri: String).returns(String) }
206
276
  def self.scp_to_standard(uri)
207
277
  return uri unless uri.start_with?("git@")
208
278
 
209
- "https://#{uri.split('git@').last.sub(%r{:/?}, '/')}"
279
+ "https://#{T.must(uri.split('git@').last).sub(%r{:/?}, '/')}"
210
280
  end
211
281
 
282
+ sig { returns(String) }
212
283
  def self.credential_helper_path
213
284
  File.join(__dir__, "../../bin/git-credential-store-immutable")
214
285
  end
215
286
 
216
- # rubocop:disable Metrics/AbcSize
217
287
  # rubocop:disable Metrics/PerceivedComplexity
288
+ sig { params(credentials: T::Array[T::Hash[String, String]], safe_directories: T::Array[String]).void }
218
289
  def self.configure_git_to_use_https_with_credentials(credentials, safe_directories)
219
290
  File.open(GIT_CONFIG_GLOBAL_PATH, "w") do |file|
220
291
  file << "# Generated by dependabot/dependabot-core"
@@ -274,6 +345,7 @@ module Dependabot
274
345
  # rubocop:enable Metrics/AbcSize
275
346
  # rubocop:enable Metrics/PerceivedComplexity
276
347
 
348
+ sig { params(host: String).void }
277
349
  def self.configure_git_to_use_https(host)
278
350
  # NOTE: we use --global here (rather than --system) so that Dependabot
279
351
  # can be run without privileged access
@@ -299,6 +371,7 @@ module Dependabot
299
371
  )
300
372
  end
301
373
 
374
+ sig { params(path: String).void }
302
375
  def self.reset_git_repo(path)
303
376
  Dir.chdir(path) do
304
377
  run_shell_command("git reset HEAD --hard")
@@ -306,6 +379,7 @@ module Dependabot
306
379
  end
307
380
  end
308
381
 
382
+ sig { returns(T::Array[String]) }
309
383
  def self.find_safe_directories
310
384
  # to preserve safe directories from global .gitconfig
311
385
  output, process = Open3.capture2("git config --global --get-all safe.directory")
@@ -314,6 +388,15 @@ module Dependabot
314
388
  safe_directories
315
389
  end
316
390
 
391
+ sig do
392
+ params(
393
+ command: String,
394
+ allow_unsafe_shell_command: T::Boolean,
395
+ env: T.nilable(T::Hash[String, String]),
396
+ fingerprint: T.nilable(String),
397
+ stderr_to_stdout: T::Boolean
398
+ ).returns(String)
399
+ end
317
400
  def self.run_shell_command(command,
318
401
  allow_unsafe_shell_command: false,
319
402
  env: {},
@@ -347,6 +430,7 @@ module Dependabot
347
430
  )
348
431
  end
349
432
 
433
+ sig { params(command: String, stdin_data: String, env: T.nilable(T::Hash[String, String])).returns(String) }
350
434
  def self.helper_subprocess_bash_command(command:, stdin_data:, env:)
351
435
  escaped_stdin_data = stdin_data.gsub("\"", "\\\"")
352
436
  env_keys = env ? env.compact.map { |k, v| "#{k}=#{v}" }.join(" ") + " " : ""
@@ -1,16 +1,35 @@
1
- # typed: true
1
+ # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
+ require "sorbet-runtime"
5
+
4
6
  module Dependabot
5
7
  module SimpleInstrumentor
6
8
  class << self
7
- attr_accessor :events, :subscribers
9
+ extend T::Sig
10
+ extend T::Generic
11
+
12
+ sig { returns(T.nilable(T::Array[T.proc.params(name: String, params: T::Hash[Symbol, T.untyped]).void])) }
13
+ attr_accessor :subscribers
8
14
 
15
+ sig { params(block: T.proc.params(name: String, params: T::Hash[Symbol, T.untyped]).void).void }
9
16
  def subscribe(&block)
10
- @subscribers ||= []
17
+ @subscribers ||= T.let(
18
+ [],
19
+ T.nilable(T::Array[T.proc.params(name: String, params: T::Hash[Symbol, T.untyped]).void])
20
+ )
11
21
  @subscribers << block
12
22
  end
13
23
 
24
+ sig do
25
+ type_parameters(:T)
26
+ .params(
27
+ name: String,
28
+ params: T::Hash[Symbol, T.untyped],
29
+ block: T.proc.returns(T.type_parameter(:T))
30
+ )
31
+ .returns(T.nilable(T.type_parameter(:T)))
32
+ end
14
33
  def instrument(name, params = {}, &block)
15
34
  @subscribers&.each { |s| s.call(name, params) }
16
35
  yield if block
@@ -1,8 +1,12 @@
1
- # typed: true
1
+ # typed: strict
2
2
  # frozen_string_literal: true
3
3
 
4
+ require "sorbet-runtime"
5
+
4
6
  module Dependabot
5
7
  class Source
8
+ extend T::Sig
9
+
6
10
  GITHUB_SOURCE = %r{
7
11
  (?<provider>github)
8
12
  (?:\.com)[/:]
@@ -59,24 +63,48 @@ module Dependabot
59
63
  (?:#{CODECOMMIT_SOURCE})
60
64
  /x
61
65
 
62
- IGNORED_PROVIDER_HOSTS = %w(gitbox.apache.org svn.apache.org fuchsia.googlesource.com).freeze
66
+ IGNORED_PROVIDER_HOSTS = T.let(%w(gitbox.apache.org svn.apache.org fuchsia.googlesource.com).freeze,
67
+ T::Array[String])
68
+
69
+ sig { returns(String) }
70
+ attr_accessor :provider
71
+
72
+ sig { returns(String) }
73
+ attr_accessor :repo
74
+
75
+ sig { returns(T.nilable(String)) }
76
+ attr_accessor :directory
63
77
 
64
- attr_accessor :provider, :repo, :directory, :branch, :commit,
65
- :hostname, :api_endpoint
78
+ sig { returns(T.nilable(T::Array[String])) }
79
+ attr_accessor :directories
66
80
 
81
+ sig { returns(T.nilable(String)) }
82
+ attr_accessor :branch
83
+
84
+ sig { returns(T.nilable(String)) }
85
+ attr_accessor :commit
86
+
87
+ sig { returns(String) }
88
+ attr_accessor :hostname
89
+
90
+ sig { returns(T.nilable(String)) }
91
+ attr_accessor :api_endpoint
92
+
93
+ sig { params(url_string: T.nilable(String)).returns(T.nilable(Source)) }
67
94
  def self.from_url(url_string)
68
95
  return github_enterprise_from_url(url_string) unless url_string&.match?(SOURCE_REGEX)
69
96
 
70
- captures = url_string.match(SOURCE_REGEX).named_captures
97
+ captures = T.must(url_string.match(SOURCE_REGEX)).named_captures
71
98
 
72
99
  new(
73
- provider: captures.fetch("provider"),
74
- repo: captures.fetch("repo").delete_suffix(".git").delete_suffix("."),
100
+ provider: T.must(captures.fetch("provider")),
101
+ repo: T.must(captures.fetch("repo")).delete_suffix(".git").delete_suffix("."),
75
102
  directory: captures.fetch("directory"),
76
103
  branch: captures.fetch("branch")
77
104
  )
78
105
  end
79
106
 
107
+ sig { params(url_string: T.nilable(String)).returns(T.nilable(Source)) }
80
108
  def self.github_enterprise_from_url(url_string)
81
109
  captures = url_string&.match(GITHUB_ENTERPRISE_SOURCE)&.named_captures
82
110
  return unless captures
@@ -88,7 +116,7 @@ module Dependabot
88
116
 
89
117
  new(
90
118
  provider: "github",
91
- repo: captures.fetch("repo").delete_suffix(".git").delete_suffix("."),
119
+ repo: T.must(captures.fetch("repo")).delete_suffix(".git").delete_suffix("."),
92
120
  directory: captures.fetch("directory"),
93
121
  branch: captures.fetch("branch"),
94
122
  hostname: captures.fetch("host"),
@@ -96,18 +124,30 @@ module Dependabot
96
124
  )
97
125
  end
98
126
 
127
+ sig { params(base_url: String).returns(T::Boolean) }
99
128
  def self.github_enterprise?(base_url)
100
129
  resp = Excon.get(File.join(base_url, "status"))
101
130
  resp.status == 200 &&
102
131
  # Alternatively: resp.headers["Server"] == "GitHub.com", but this
103
132
  # currently doesn't work with development environments
104
- resp.headers["X-GitHub-Request-Id"] &&
105
- !resp.headers["X-GitHub-Request-Id"].empty?
133
+ ((resp.headers["X-GitHub-Request-Id"] && !resp.headers["X-GitHub-Request-Id"]&.empty?) || false)
106
134
  rescue StandardError
107
135
  false
108
136
  end
109
137
 
110
- def initialize(provider:, repo:, directory: nil, branch: nil, commit: nil,
138
+ sig do
139
+ params(
140
+ provider: String,
141
+ repo: String,
142
+ directory: T.nilable(String),
143
+ directories: T.nilable(T::Array[String]),
144
+ branch: T.nilable(String),
145
+ commit: T.nilable(String),
146
+ hostname: T.nilable(String),
147
+ api_endpoint: T.nilable(String)
148
+ ).void
149
+ end
150
+ def initialize(provider:, repo:, directory: nil, directories: nil, branch: nil, commit: nil,
111
151
  hostname: nil, api_endpoint: nil)
112
152
  if (hostname.nil? ^ api_endpoint.nil?) && (provider != "codecommit")
113
153
  msg = "Both hostname and api_endpoint must be specified if either " \
@@ -119,16 +159,19 @@ module Dependabot
119
159
  @provider = provider
120
160
  @repo = repo
121
161
  @directory = directory
162
+ @directories = directories
122
163
  @branch = branch
123
164
  @commit = commit
124
- @hostname = hostname || default_hostname(provider)
125
- @api_endpoint = api_endpoint || default_api_endpoint(provider)
165
+ @hostname = T.let(hostname || default_hostname(provider), String)
166
+ @api_endpoint = T.let(api_endpoint || default_api_endpoint(provider), T.nilable(String))
126
167
  end
127
168
 
169
+ sig { returns(String) }
128
170
  def url
129
171
  "https://" + hostname + "/" + repo
130
172
  end
131
173
 
174
+ sig { returns(String) }
132
175
  def url_with_directory
133
176
  return url if [nil, ".", "/"].include?(directory)
134
177
 
@@ -149,25 +192,29 @@ module Dependabot
149
192
  end
150
193
  end
151
194
 
195
+ sig { returns(String) }
152
196
  def organization
153
- repo.split("/").first
197
+ T.must(repo.split("/").first)
154
198
  end
155
199
 
200
+ sig { returns(String) }
156
201
  def project
157
202
  raise "Project is an Azure DevOps concept only" unless provider == "azure"
158
203
 
159
204
  parts = repo.split("/_git/")
160
- return parts.first.split("/").last if parts.first.split("/").count == 2
205
+ return T.must(T.must(parts.first).split("/").last) if parts.first&.split("/")&.count == 2
161
206
 
162
- parts.last
207
+ T.must(parts.last)
163
208
  end
164
209
 
210
+ sig { returns(String) }
165
211
  def unscoped_repo
166
- repo.split("/").last
212
+ T.must(repo.split("/").last)
167
213
  end
168
214
 
169
215
  private
170
216
 
217
+ sig { params(provider: String).returns(String) }
171
218
  def default_hostname(provider)
172
219
  case provider
173
220
  when "github" then "github.com"
@@ -179,6 +226,7 @@ module Dependabot
179
226
  end
180
227
  end
181
228
 
229
+ sig { params(provider: String).returns(T.nilable(String)) }
182
230
  def default_api_endpoint(provider)
183
231
  case provider
184
232
  when "github" then "https://api.github.com/"
@@ -1,9 +1,20 @@
1
- # typed: true
1
+ # typed: strong
2
2
  # frozen_string_literal: true
3
3
 
4
+ require "sorbet-runtime"
5
+
4
6
  module Dependabot
5
7
  module UpdateCheckers
6
8
  module VersionFilters
9
+ extend T::Sig
10
+
11
+ sig do
12
+ params(
13
+ versions_array: T::Array[T.any(Gem::Version, T::Hash[Symbol, String])],
14
+ security_advisories: T::Array[SecurityAdvisory]
15
+ )
16
+ .returns(T::Array[T.any(Gem::Version, T::Hash[Symbol, String])])
17
+ end
7
18
  def self.filter_vulnerable_versions(versions_array, security_advisories)
8
19
  versions_array.reject do |v|
9
20
  security_advisories.any? do |a|