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.
- 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
|