dependabot-common 0.95.1 → 0.95.2

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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/lib/dependabot.rb +4 -0
  3. data/lib/dependabot/clients/bitbucket.rb +105 -0
  4. data/lib/dependabot/clients/github_with_retries.rb +121 -0
  5. data/lib/dependabot/clients/gitlab.rb +72 -0
  6. data/lib/dependabot/dependency.rb +115 -0
  7. data/lib/dependabot/dependency_file.rb +60 -0
  8. data/lib/dependabot/errors.rb +179 -0
  9. data/lib/dependabot/file_fetchers.rb +18 -0
  10. data/lib/dependabot/file_fetchers/README.md +65 -0
  11. data/lib/dependabot/file_fetchers/base.rb +368 -0
  12. data/lib/dependabot/file_parsers.rb +18 -0
  13. data/lib/dependabot/file_parsers/README.md +45 -0
  14. data/lib/dependabot/file_parsers/base.rb +31 -0
  15. data/lib/dependabot/file_parsers/base/dependency_set.rb +77 -0
  16. data/lib/dependabot/file_updaters.rb +18 -0
  17. data/lib/dependabot/file_updaters/README.md +58 -0
  18. data/lib/dependabot/file_updaters/base.rb +52 -0
  19. data/lib/dependabot/git_commit_checker.rb +412 -0
  20. data/lib/dependabot/metadata_finders.rb +18 -0
  21. data/lib/dependabot/metadata_finders/README.md +53 -0
  22. data/lib/dependabot/metadata_finders/base.rb +117 -0
  23. data/lib/dependabot/metadata_finders/base/changelog_finder.rb +321 -0
  24. data/lib/dependabot/metadata_finders/base/changelog_pruner.rb +177 -0
  25. data/lib/dependabot/metadata_finders/base/commits_finder.rb +221 -0
  26. data/lib/dependabot/metadata_finders/base/release_finder.rb +255 -0
  27. data/lib/dependabot/pull_request_creator.rb +155 -0
  28. data/lib/dependabot/pull_request_creator/branch_namer.rb +170 -0
  29. data/lib/dependabot/pull_request_creator/commit_signer.rb +63 -0
  30. data/lib/dependabot/pull_request_creator/github.rb +277 -0
  31. data/lib/dependabot/pull_request_creator/gitlab.rb +162 -0
  32. data/lib/dependabot/pull_request_creator/labeler.rb +373 -0
  33. data/lib/dependabot/pull_request_creator/message_builder.rb +906 -0
  34. data/lib/dependabot/pull_request_updater.rb +43 -0
  35. data/lib/dependabot/pull_request_updater/github.rb +165 -0
  36. data/lib/dependabot/shared_helpers.rb +224 -0
  37. data/lib/dependabot/source.rb +120 -0
  38. data/lib/dependabot/update_checkers.rb +18 -0
  39. data/lib/dependabot/update_checkers/README.md +67 -0
  40. data/lib/dependabot/update_checkers/base.rb +220 -0
  41. data/lib/dependabot/utils.rb +33 -0
  42. data/lib/dependabot/version.rb +5 -0
  43. data/lib/rubygems_version_patch.rb +14 -0
  44. metadata +44 -2
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/shared_helpers"
4
+
5
+ module Dependabot
6
+ class DependabotError < StandardError
7
+ def initialize(msg = nil)
8
+ msg = sanitize_message(msg)
9
+ super(msg)
10
+ end
11
+
12
+ private
13
+
14
+ def sanitize_message(message)
15
+ return unless message
16
+
17
+ path_regex =
18
+ Regexp.escape(SharedHelpers::BUMP_TMP_DIR_PATH) + "\/" +
19
+ Regexp.escape(SharedHelpers::BUMP_TMP_FILE_PREFIX) + "[^/]*"
20
+
21
+ message.gsub(/#{path_regex}/, "dependabot_tmp_dir")
22
+ end
23
+ end
24
+
25
+ class OutOfMemory < DependabotError; end
26
+
27
+ #####################
28
+ # Repo leval errors #
29
+ #####################
30
+
31
+ class BranchNotFound < DependabotError
32
+ attr_reader :branch_name
33
+
34
+ def initialize(branch_name, msg = nil)
35
+ @branch_name = branch_name
36
+ msg = sanitize_message(msg)
37
+ super(msg)
38
+ end
39
+ end
40
+
41
+ class RepoNotFound < DependabotError
42
+ attr_reader :source
43
+
44
+ def initialize(source, msg = nil)
45
+ @source = source
46
+ super(msg)
47
+ end
48
+ end
49
+
50
+ #####################
51
+ # File level errors #
52
+ #####################
53
+
54
+ class DependencyFileNotFound < DependabotError
55
+ attr_reader :file_path
56
+
57
+ def initialize(file_path, msg = nil)
58
+ @file_path = file_path
59
+ super(msg)
60
+ end
61
+
62
+ def file_name
63
+ file_path.split("/").last
64
+ end
65
+
66
+ def directory
67
+ # Directory should always start with a `/`
68
+ file_path.split("/")[0..-2].join("/").sub(%r{^/*}, "/")
69
+ end
70
+ end
71
+
72
+ class DependencyFileNotParseable < DependabotError
73
+ attr_reader :file_path
74
+
75
+ def initialize(file_path, msg = nil)
76
+ @file_path = file_path
77
+ super(msg)
78
+ end
79
+
80
+ def file_name
81
+ file_path.split("/").last
82
+ end
83
+
84
+ def directory
85
+ # Directory should always start with a `/`
86
+ file_path.split("/")[0..-2].join("/").sub(%r{^/*}, "/")
87
+ end
88
+ end
89
+
90
+ class DependencyFileNotEvaluatable < DependabotError; end
91
+ class DependencyFileNotResolvable < DependabotError; end
92
+
93
+ #######################
94
+ # Source level errors #
95
+ #######################
96
+
97
+ class PrivateSourceAuthenticationFailure < DependabotError
98
+ attr_reader :source
99
+
100
+ def initialize(source)
101
+ @source = source
102
+ msg = "The following source could not be reached as it requires "\
103
+ "authentication (and any provided details were invalid or lacked "\
104
+ "the required permissions): #{source}"
105
+ super(msg)
106
+ end
107
+ end
108
+
109
+ class PrivateSourceTimedOut < DependabotError
110
+ attr_reader :source
111
+
112
+ def initialize(source)
113
+ @source = source
114
+ super("The following source timed out: #{source}")
115
+ end
116
+ end
117
+
118
+ class PrivateSourceCertificateFailure < DependabotError
119
+ attr_reader :source
120
+
121
+ def initialize(source)
122
+ @source = source
123
+ super("Could not verify the SSL certificate for #{source}")
124
+ end
125
+ end
126
+
127
+ class MissingEnvironmentVariable < DependabotError
128
+ attr_reader :environment_variable
129
+
130
+ def initialize(environment_variable)
131
+ @environment_variable = environment_variable
132
+ super("Missing environment variable #{environment_variable}")
133
+ end
134
+ end
135
+
136
+ # Useful for JS file updaters, where the registry API sometimes returns
137
+ # different results to the actual update process
138
+ class InconsistentRegistryResponse < DependabotError; end
139
+
140
+ ###########################
141
+ # Dependency level errors #
142
+ ###########################
143
+
144
+ class GitDependenciesNotReachable < DependabotError
145
+ attr_reader :dependency_urls
146
+
147
+ def initialize(*dependency_urls)
148
+ @dependency_urls =
149
+ dependency_urls.flatten.map { |uri| uri.gsub(/x-access-token.*?@/, "") }
150
+
151
+ msg = "The following git URLs could not be retrieved: "\
152
+ "#{dependency_urls.join(', ')}"
153
+ super(msg)
154
+ end
155
+ end
156
+
157
+ class GitDependencyReferenceNotFound < DependabotError
158
+ attr_reader :dependency
159
+
160
+ def initialize(dependency)
161
+ @dependency = dependency
162
+
163
+ msg = "The branch or reference specified for #{dependency} could not "\
164
+ "be retrieved"
165
+ super(msg)
166
+ end
167
+ end
168
+
169
+ class PathDependenciesNotReachable < DependabotError
170
+ attr_reader :dependencies
171
+
172
+ def initialize(*dependencies)
173
+ @dependencies = dependencies.flatten
174
+ msg = "The following path based dependencies could not be retrieved: "\
175
+ "#{dependencies.join(', ')}"
176
+ super(msg)
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dependabot
4
+ module FileFetchers
5
+ @file_fetchers = {}
6
+
7
+ def self.for_package_manager(package_manager)
8
+ file_fetcher = @file_fetchers[package_manager]
9
+ return file_fetcher if file_fetcher
10
+
11
+ raise "Unsupported package_manager #{package_manager}"
12
+ end
13
+
14
+ def self.register(package_manager, file_fetcher)
15
+ @file_fetchers[package_manager] = file_fetcher
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,65 @@
1
+ # File fetchers
2
+
3
+ File fetchers are used to fetch the relevant dependency files for a project
4
+ (e.g., the `Gemfile` and `Gemfile.lock`). They are also responsible for checking
5
+ whether a repo has an admissable set of requirement files.
6
+
7
+ There is a `Dependabot::FileFetchers` class for each language Dependabot
8
+ supports.
9
+
10
+ ## Public API
11
+
12
+ Each `Dependabot::FileFetchers` class implements the following methods:
13
+
14
+ | Method | Description |
15
+ |----------------------------------|-----------------------------------------------------------------------------------------------|
16
+ | `.required_files_in?` | Checks an array of filenames (string) and returns a boolean describing whether the language-specific dependency files required for an update run are present. |
17
+ | `.required_files_message` | Returns a static error message which can be displayed to a user if `required_files_in?` returns false. |
18
+ | `#files` | Fetches the language-specific dependency files for the repo this instance was created with. Returns an array of `Dependabot::DependencyFile` instances. |
19
+ | `#commit` | Returns the commit SHA-1 hash at the time the dependency files were fetched. If called before `files`, the returned value will be used in subsequent calls to `files`. |
20
+
21
+
22
+ An integration might look as follows:
23
+
24
+ ```ruby
25
+ require 'octokit'
26
+ require 'dependabot/file_fetchers'
27
+ require 'dependabot/source'
28
+
29
+ target_repo_name = 'dependabot/dependabot-core'
30
+ source = Dependabot::Source.new(provider: 'github', repo: target_repo_name)
31
+
32
+ client = Octokit::Client.new
33
+ fetcher_class = Dependabot::FileFetchers::Ruby::Bundler
34
+ filenames = client.contents(target_repo_name).map(&:name)
35
+
36
+ unless fetcher_class.required_files_in?(filenames)
37
+ raise fetcher_class.required_files_message
38
+ end
39
+
40
+ fetcher = fetcher_class.new(source: source, credentials: [])
41
+
42
+ puts "Fetched #{fetcher.files.map(&:name)}, at commit SHA-1 '#{fetcher.commit}'"
43
+ ```
44
+
45
+ ## Writing a file fetcher for a new language
46
+
47
+ All new file fetchers should inherit from `Dependabot::FileFetchers::Base` and
48
+ implement the following methods:
49
+
50
+ | Method | Description |
51
+ |----------------------------------|-----------------------------------------------------------------------------------------------|
52
+ | `.required_files_in?` | See Public API section. |
53
+ | `.required_files_message` | See Public API section. |
54
+ | `#fetch_files` | Private method to fetch the required files from GitHub. For each required file, you can use the `fetch_file_from_host(filename)` method from `Dependabot::FileFetchers::Base` to do the fetching. |
55
+
56
+ To ensure the above are implemented, you should include
57
+ `it_behaves_like "a dependency file fetcher"` in your specs for the new file
58
+ fetcher.
59
+
60
+ File fetchers tend to get complicated when the file requirements for an update
61
+ to run are non-trivial - for example, for Ruby we could accept
62
+ [`Gemfile`, `Gemfile.lock`] or [`Gemfile`, `example.gemspec`],
63
+ but not just [`Gemfile.lock`]. When adding a new lanugage, it's normally easiest
64
+ to pick a single case and implement it for all the update steps (parsing, update
65
+ checking, etc.). You can then return and add other cases later.
@@ -0,0 +1,368 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/dependency_file"
4
+ require "dependabot/source"
5
+ require "dependabot/errors"
6
+ require "dependabot/clients/github_with_retries"
7
+ require "dependabot/clients/bitbucket"
8
+ require "dependabot/clients/gitlab"
9
+ require "dependabot/shared_helpers"
10
+
11
+ # rubocop:disable Metrics/ClassLength
12
+ module Dependabot
13
+ module FileFetchers
14
+ class Base
15
+ attr_reader :source, :credentials
16
+
17
+ CLIENT_NOT_FOUND_ERRORS = [
18
+ Octokit::NotFound,
19
+ Gitlab::Error::NotFound,
20
+ Dependabot::Clients::Bitbucket::NotFound
21
+ ].freeze
22
+
23
+ def self.required_files_in?(_filename_array)
24
+ raise NotImplementedError
25
+ end
26
+
27
+ def self.required_files_message
28
+ raise NotImplementedError
29
+ end
30
+
31
+ def initialize(source:, credentials:)
32
+ @source = source
33
+ @credentials = credentials
34
+
35
+ @submodule_directories = {}
36
+ end
37
+
38
+ def repo
39
+ source.repo
40
+ end
41
+
42
+ def directory
43
+ source.directory || "/"
44
+ end
45
+
46
+ def target_branch
47
+ source.branch
48
+ end
49
+
50
+ def files
51
+ @files ||= fetch_files
52
+ end
53
+
54
+ def commit
55
+ branch = target_branch || default_branch_for_repo
56
+
57
+ @commit ||= client_for_provider.fetch_commit(repo, branch)
58
+ rescue *CLIENT_NOT_FOUND_ERRORS
59
+ raise Dependabot::BranchNotFound, branch
60
+ rescue Octokit::Conflict => error
61
+ raise unless error.message.include?("Repository is empty")
62
+ end
63
+
64
+ private
65
+
66
+ def fetch_file_if_present(filename, fetch_submodules: false)
67
+ dir = File.dirname(filename)
68
+ basename = File.basename(filename)
69
+
70
+ repo_includes_basename =
71
+ repo_contents(dir: dir, fetch_submodules: fetch_submodules).
72
+ reject { |f| f.type == "dir" }.
73
+ map(&:name).include?(basename)
74
+ return unless repo_includes_basename
75
+
76
+ fetch_file_from_host(filename, fetch_submodules: fetch_submodules)
77
+ rescue *CLIENT_NOT_FOUND_ERRORS
78
+ path = Pathname.new(File.join(directory, filename)).cleanpath.to_path
79
+ raise Dependabot::DependencyFileNotFound, path
80
+ end
81
+
82
+ def fetch_file_from_host(filename, type: "file", fetch_submodules: false)
83
+ path = Pathname.new(File.join(directory, filename)).cleanpath.to_path
84
+
85
+ DependencyFile.new(
86
+ name: Pathname.new(filename).cleanpath.to_path,
87
+ directory: directory,
88
+ type: type,
89
+ content: _fetch_file_content(path, fetch_submodules: fetch_submodules)
90
+ )
91
+ rescue *CLIENT_NOT_FOUND_ERRORS
92
+ raise Dependabot::DependencyFileNotFound, path
93
+ end
94
+
95
+ def repo_contents(dir: ".", ignore_base_directory: false,
96
+ raise_errors: true, fetch_submodules: false)
97
+ dir = File.join(directory, dir) unless ignore_base_directory
98
+ path = Pathname.new(File.join(dir)).cleanpath.to_path.gsub(%r{^/*}, "")
99
+
100
+ @repo_contents ||= {}
101
+ @repo_contents[dir] ||= _fetch_repo_contents(
102
+ path,
103
+ raise_errors: raise_errors,
104
+ fetch_submodules: fetch_submodules
105
+ )
106
+ end
107
+
108
+ #################################################
109
+ # INTERNAL METHODS (not for use by sub-classes) #
110
+ #################################################
111
+
112
+ def _fetch_repo_contents(path, fetch_submodules: false,
113
+ raise_errors: true)
114
+ path = path.gsub(" ", "%20")
115
+ provider, repo, tmp_path, commit =
116
+ _full_specification_for(path, fetch_submodules: fetch_submodules).
117
+ values_at(:provider, :repo, :path, :commit)
118
+
119
+ _fetch_repo_contents_fully_specified(provider, repo, tmp_path, commit)
120
+ rescue *CLIENT_NOT_FOUND_ERRORS
121
+ result = raise_errors ? -> { raise } : -> { [] }
122
+ retrying ||= false
123
+
124
+ # If the path changes after calling _fetch_repo_contents_fully_specified
125
+ # it's because we've found a sub-module (and are fetching them). Trigger
126
+ # a retry to get its contents.
127
+ updated_path =
128
+ _full_specification_for(path, fetch_submodules: fetch_submodules).
129
+ fetch(:path)
130
+ retry if updated_path != tmp_path
131
+
132
+ return result.call unless fetch_submodules && !retrying
133
+
134
+ _find_submodules(path)
135
+ return result.call unless _submodule_for(path)
136
+
137
+ retrying = true
138
+ retry
139
+ end
140
+
141
+ def _fetch_repo_contents_fully_specified(provider, repo, path, commit)
142
+ case provider
143
+ when "github"
144
+ _github_repo_contents(repo, path, commit)
145
+ when "gitlab"
146
+ _gitlab_repo_contents(repo, path, commit)
147
+ when "bitbucket"
148
+ _bitbucket_repo_contents(repo, path, commit)
149
+ else raise "Unsupported provider '#{provider}'."
150
+ end
151
+ end
152
+
153
+ def _github_repo_contents(repo, path, commit)
154
+ path = path.gsub(" ", "%20")
155
+ github_response = github_client.contents(repo, path: path, ref: commit)
156
+
157
+ if github_response.respond_to?(:type) &&
158
+ github_response.type == "submodule"
159
+ @submodule_directories[path] = github_response
160
+ raise Octokit::NotFound
161
+ elsif github_response.respond_to?(:type)
162
+ raise Octokit::NotFound
163
+ end
164
+
165
+ github_response.map { |f| _build_github_file_struct(f) }
166
+ end
167
+
168
+ def _build_github_file_struct(file)
169
+ OpenStruct.new(
170
+ name: file.name,
171
+ path: file.path,
172
+ type: file.type,
173
+ sha: file.sha,
174
+ size: file.size
175
+ )
176
+ end
177
+
178
+ def _gitlab_repo_contents(repo, path, commit)
179
+ gitlab_client.
180
+ repo_tree(repo, path: path, ref_name: commit, per_page: 100).
181
+ map do |file|
182
+ OpenStruct.new(
183
+ name: file.name,
184
+ path: file.path,
185
+ type: file.type == "blob" ? "file" : file.type,
186
+ size: 0 # GitLab doesn't return file size
187
+ )
188
+ end
189
+ end
190
+
191
+ def _bitbucket_repo_contents(repo, path, commit)
192
+ response = bitbucket_client.fetch_repo_contents(
193
+ repo,
194
+ commit,
195
+ path
196
+ )
197
+
198
+ response.map do |file|
199
+ type = case file.fetch("type")
200
+ when "commit_file" then "file"
201
+ when "commit_directory" then "dir"
202
+ else file.fetch("type")
203
+ end
204
+
205
+ OpenStruct.new(
206
+ name: File.basename(file.fetch("path")),
207
+ path: file.fetch("path"),
208
+ type: type,
209
+ size: file.fetch("size", 0)
210
+ )
211
+ end
212
+ end
213
+
214
+ def _full_specification_for(path, fetch_submodules:)
215
+ if fetch_submodules && _submodule_for(path) &&
216
+ Source.from_url(
217
+ @submodule_directories[_submodule_for(path)].submodule_git_url
218
+ )
219
+ submodule_details = @submodule_directories[_submodule_for(path)]
220
+ sub_source = Source.from_url(submodule_details.submodule_git_url)
221
+ {
222
+ repo: sub_source.repo,
223
+ commit: submodule_details.sha,
224
+ provider: sub_source.provider,
225
+ path: path.gsub(%r{^#{Regexp.quote(_submodule_for(path))}(/|$)}, "")
226
+ }
227
+ else
228
+ {
229
+ repo: source.repo,
230
+ path: path,
231
+ commit: commit,
232
+ provider: source.provider
233
+ }
234
+ end
235
+ end
236
+
237
+ def _fetch_file_content(path, fetch_submodules: false)
238
+ path = path.gsub(%r{^/*}, "")
239
+
240
+ provider, repo, path, commit =
241
+ _full_specification_for(path, fetch_submodules: fetch_submodules).
242
+ values_at(:provider, :repo, :path, :commit)
243
+
244
+ _fetch_file_content_fully_specified(provider, repo, path, commit)
245
+ rescue *CLIENT_NOT_FOUND_ERRORS
246
+ retrying ||= false
247
+
248
+ raise unless fetch_submodules && !retrying && !_submodule_for(path)
249
+
250
+ _find_submodules(path)
251
+ raise unless _submodule_for(path)
252
+
253
+ retrying = true
254
+ retry
255
+ end
256
+
257
+ def _fetch_file_content_fully_specified(provider, repo, path, commit)
258
+ case provider
259
+ when "github"
260
+ _fetch_file_content_from_github(path, repo, commit)
261
+ when "gitlab"
262
+ tmp = gitlab_client.get_file(repo, path, commit).content
263
+ Base64.decode64(tmp).force_encoding("UTF-8").encode
264
+ when "bitbucket"
265
+ bitbucket_client.fetch_file_contents(repo, commit, path)
266
+ else raise "Unsupported provider '#{source.provider}'."
267
+ end
268
+ end
269
+
270
+ # rubocop:disable Metrics/AbcSize
271
+ def _fetch_file_content_from_github(path, repo, commit)
272
+ tmp = github_client.contents(repo, path: path, ref: commit)
273
+
274
+ raise Octokit::NotFound if tmp.is_a?(Array)
275
+
276
+ if tmp.type == "symlink"
277
+ tmp = github_client.contents(
278
+ repo,
279
+ path: tmp.target,
280
+ ref: commit
281
+ )
282
+ end
283
+
284
+ Base64.decode64(tmp.content).force_encoding("UTF-8").encode
285
+ rescue Octokit::Forbidden => error
286
+ raise unless error.message.include?("too_large")
287
+
288
+ # Fall back to Git Data API to fetch the file
289
+ prefix_dir = directory.gsub(%r{(^/|/$)}, "")
290
+ dir = File.dirname(path).gsub(%r{^/?#{Regexp.escape(prefix_dir)}/?}, "")
291
+ basename = File.basename(path)
292
+ file_details = repo_contents(dir: dir).find { |f| f.name == basename }
293
+ raise unless file_details
294
+
295
+ tmp = github_client.blob(repo, file_details.sha)
296
+ return tmp.content if tmp.encoding == "utf-8"
297
+
298
+ Base64.decode64(tmp.content).force_encoding("UTF-8").encode
299
+ end
300
+ # rubocop:enable Metrics/AbcSize
301
+
302
+ def default_branch_for_repo
303
+ @default_branch_for_repo ||= client_for_provider.
304
+ fetch_default_branch(repo)
305
+ rescue *CLIENT_NOT_FOUND_ERRORS
306
+ raise Dependabot::RepoNotFound, source
307
+ end
308
+
309
+ # Update the @submodule_directories hash by exploiting a side-effect of
310
+ # recursively calling `repo_contents` for each directory up the tree
311
+ # until a submodule is found
312
+ def _find_submodules(path)
313
+ path = Pathname.new(path).cleanpath.to_path.gsub(%r{^/*}, "")
314
+ dir = File.dirname(path)
315
+
316
+ return if [directory, "."].include?(dir)
317
+
318
+ repo_contents(
319
+ dir: dir,
320
+ ignore_base_directory: true,
321
+ fetch_submodules: true,
322
+ raise_errors: false
323
+ )
324
+ end
325
+
326
+ def _submodule_for(path)
327
+ submodules = @submodule_directories.keys
328
+ submodules.
329
+ select { |k| path.match?(%r{^#{Regexp.quote(k)}(/|$)}) }.
330
+ max_by(&:length)
331
+ end
332
+
333
+ def client_for_provider
334
+ case source.provider
335
+ when "github" then github_client
336
+ when "gitlab" then gitlab_client
337
+ when "bitbucket" then bitbucket_client
338
+ else raise "Unsupported provider '#{source.provider}'."
339
+ end
340
+ end
341
+
342
+ def github_client
343
+ @github_client ||=
344
+ Dependabot::Clients::GithubWithRetries.for_source(
345
+ source: source,
346
+ credentials: credentials
347
+ )
348
+ end
349
+
350
+ def gitlab_client
351
+ @gitlab_client ||=
352
+ Dependabot::Clients::Gitlab.for_source(
353
+ source: source,
354
+ credentials: credentials
355
+ )
356
+ end
357
+
358
+ def bitbucket_client
359
+ # TODO: When self-hosted Bitbucket is supported this should use
360
+ # `Bitbucket.for_source`
361
+ @bitbucket_client ||=
362
+ Dependabot::Clients::Bitbucket.
363
+ for_bitbucket_dot_org(credentials: credentials)
364
+ end
365
+ end
366
+ end
367
+ end
368
+ # rubocop:enable Metrics/ClassLength