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.
- checksums.yaml +4 -4
- data/lib/dependabot.rb +4 -0
- data/lib/dependabot/clients/bitbucket.rb +105 -0
- data/lib/dependabot/clients/github_with_retries.rb +121 -0
- data/lib/dependabot/clients/gitlab.rb +72 -0
- data/lib/dependabot/dependency.rb +115 -0
- data/lib/dependabot/dependency_file.rb +60 -0
- data/lib/dependabot/errors.rb +179 -0
- data/lib/dependabot/file_fetchers.rb +18 -0
- data/lib/dependabot/file_fetchers/README.md +65 -0
- data/lib/dependabot/file_fetchers/base.rb +368 -0
- data/lib/dependabot/file_parsers.rb +18 -0
- data/lib/dependabot/file_parsers/README.md +45 -0
- data/lib/dependabot/file_parsers/base.rb +31 -0
- data/lib/dependabot/file_parsers/base/dependency_set.rb +77 -0
- data/lib/dependabot/file_updaters.rb +18 -0
- data/lib/dependabot/file_updaters/README.md +58 -0
- data/lib/dependabot/file_updaters/base.rb +52 -0
- data/lib/dependabot/git_commit_checker.rb +412 -0
- data/lib/dependabot/metadata_finders.rb +18 -0
- data/lib/dependabot/metadata_finders/README.md +53 -0
- data/lib/dependabot/metadata_finders/base.rb +117 -0
- data/lib/dependabot/metadata_finders/base/changelog_finder.rb +321 -0
- data/lib/dependabot/metadata_finders/base/changelog_pruner.rb +177 -0
- data/lib/dependabot/metadata_finders/base/commits_finder.rb +221 -0
- data/lib/dependabot/metadata_finders/base/release_finder.rb +255 -0
- data/lib/dependabot/pull_request_creator.rb +155 -0
- data/lib/dependabot/pull_request_creator/branch_namer.rb +170 -0
- data/lib/dependabot/pull_request_creator/commit_signer.rb +63 -0
- data/lib/dependabot/pull_request_creator/github.rb +277 -0
- data/lib/dependabot/pull_request_creator/gitlab.rb +162 -0
- data/lib/dependabot/pull_request_creator/labeler.rb +373 -0
- data/lib/dependabot/pull_request_creator/message_builder.rb +906 -0
- data/lib/dependabot/pull_request_updater.rb +43 -0
- data/lib/dependabot/pull_request_updater/github.rb +165 -0
- data/lib/dependabot/shared_helpers.rb +224 -0
- data/lib/dependabot/source.rb +120 -0
- data/lib/dependabot/update_checkers.rb +18 -0
- data/lib/dependabot/update_checkers/README.md +67 -0
- data/lib/dependabot/update_checkers/base.rb +220 -0
- data/lib/dependabot/utils.rb +33 -0
- data/lib/dependabot/version.rb +5 -0
- data/lib/rubygems_version_patch.rb +14 -0
- 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
|