dependabot-common 0.95.1 → 0.95.2

Sign up to get free protection for your applications and to get access to all the features.
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