dependabot-composer 0.89.0
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 +7 -0
- data/helpers/.php_cs +32 -0
- data/helpers/bin/run.php +84 -0
- data/helpers/build +14 -0
- data/helpers/composer.json +14 -0
- data/helpers/composer.lock +1528 -0
- data/helpers/php/.php_cs +34 -0
- data/helpers/setup.sh +4 -0
- data/helpers/src/DependabotInstallationManager.php +61 -0
- data/helpers/src/DependabotPluginManager.php +23 -0
- data/helpers/src/ExceptionIO.php +25 -0
- data/helpers/src/Hasher.php +21 -0
- data/helpers/src/UpdateChecker.php +123 -0
- data/helpers/src/Updater.php +97 -0
- data/lib/dependabot/composer.rb +11 -0
- data/lib/dependabot/composer/file_fetcher.rb +132 -0
- data/lib/dependabot/composer/file_parser.rb +179 -0
- data/lib/dependabot/composer/file_updater.rb +78 -0
- data/lib/dependabot/composer/file_updater/lockfile_updater.rb +267 -0
- data/lib/dependabot/composer/file_updater/manifest_updater.rb +66 -0
- data/lib/dependabot/composer/metadata_finder.rb +68 -0
- data/lib/dependabot/composer/native_helpers.rb +20 -0
- data/lib/dependabot/composer/requirement.rb +98 -0
- data/lib/dependabot/composer/update_checker.rb +176 -0
- data/lib/dependabot/composer/update_checker/requirements_updater.rb +253 -0
- data/lib/dependabot/composer/update_checker/version_resolver.rb +214 -0
- data/lib/dependabot/composer/version.rb +26 -0
- metadata +195 -0
@@ -0,0 +1,179 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dependabot/dependency"
|
4
|
+
require "dependabot/composer/version"
|
5
|
+
require "dependabot/file_parsers"
|
6
|
+
require "dependabot/file_parsers/base"
|
7
|
+
require "dependabot/shared_helpers"
|
8
|
+
require "dependabot/errors"
|
9
|
+
|
10
|
+
module Dependabot
|
11
|
+
module Composer
|
12
|
+
class FileParser < Dependabot::FileParsers::Base
|
13
|
+
require "dependabot/file_parsers/base/dependency_set"
|
14
|
+
|
15
|
+
DEPENDENCY_GROUP_KEYS = [
|
16
|
+
{
|
17
|
+
manifest: "require",
|
18
|
+
lockfile: "packages",
|
19
|
+
group: "runtime"
|
20
|
+
},
|
21
|
+
{
|
22
|
+
manifest: "require-dev",
|
23
|
+
lockfile: "packages-dev",
|
24
|
+
group: "development"
|
25
|
+
}
|
26
|
+
].freeze
|
27
|
+
|
28
|
+
def parse
|
29
|
+
dependency_set = DependencySet.new
|
30
|
+
dependency_set += manifest_dependencies
|
31
|
+
dependency_set += lockfile_dependencies
|
32
|
+
dependency_set.dependencies
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def manifest_dependencies
|
38
|
+
dependencies = DependencySet.new
|
39
|
+
|
40
|
+
DEPENDENCY_GROUP_KEYS.each do |keys|
|
41
|
+
next unless parsed_composer_json[keys[:manifest]]
|
42
|
+
|
43
|
+
parsed_composer_json[keys[:manifest]].each do |name, req|
|
44
|
+
next unless package?(name)
|
45
|
+
|
46
|
+
if lockfile
|
47
|
+
version = dependency_version(name: name, type: keys[:group])
|
48
|
+
|
49
|
+
# Ignore dependencies which appear in the composer.json but not
|
50
|
+
# the composer.lock.
|
51
|
+
next if version.nil?
|
52
|
+
|
53
|
+
# Ignore dependency versions which are non-numeric, since they
|
54
|
+
# can't be compared later in the process.
|
55
|
+
next unless version.match?(/^\d/)
|
56
|
+
end
|
57
|
+
|
58
|
+
dependencies <<
|
59
|
+
Dependency.new(
|
60
|
+
name: name,
|
61
|
+
version: dependency_version(name: name, type: keys[:group]),
|
62
|
+
requirements: [{
|
63
|
+
requirement: req,
|
64
|
+
file: "composer.json",
|
65
|
+
source: dependency_source(name: name, type: keys[:group]),
|
66
|
+
groups: [keys[:group]]
|
67
|
+
}],
|
68
|
+
package_manager: "composer"
|
69
|
+
)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
dependencies
|
74
|
+
end
|
75
|
+
|
76
|
+
def lockfile_dependencies
|
77
|
+
dependencies = DependencySet.new
|
78
|
+
|
79
|
+
return dependencies unless lockfile
|
80
|
+
|
81
|
+
DEPENDENCY_GROUP_KEYS.map { |h| h.fetch(:lockfile) }.each do |key|
|
82
|
+
next unless parsed_lockfile[key]
|
83
|
+
|
84
|
+
parsed_lockfile[key].each do |details|
|
85
|
+
name = details["name"]
|
86
|
+
next unless package?(name)
|
87
|
+
|
88
|
+
version = details["version"]&.sub(/^v?/, "")
|
89
|
+
next if version.nil?
|
90
|
+
next unless version.match?(/^\d/)
|
91
|
+
|
92
|
+
dependencies <<
|
93
|
+
Dependency.new(
|
94
|
+
name: name,
|
95
|
+
version: version,
|
96
|
+
requirements: [],
|
97
|
+
package_manager: "composer"
|
98
|
+
)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
dependencies
|
103
|
+
end
|
104
|
+
|
105
|
+
def dependency_version(name:, type:)
|
106
|
+
return unless lockfile
|
107
|
+
|
108
|
+
key = lockfile_key(type)
|
109
|
+
|
110
|
+
parsed_lockfile.
|
111
|
+
fetch(key, []).
|
112
|
+
find { |d| d["name"] == name }&.
|
113
|
+
fetch("version")&.sub(/^v?/, "")
|
114
|
+
end
|
115
|
+
|
116
|
+
def dependency_source(name:, type:)
|
117
|
+
return unless lockfile
|
118
|
+
|
119
|
+
key = lockfile_key(type)
|
120
|
+
package = parsed_lockfile.fetch(key).find { |d| d["name"] == name }
|
121
|
+
|
122
|
+
return unless package
|
123
|
+
|
124
|
+
if package["source"].nil? && package.dig("dist", "type") == "path"
|
125
|
+
return { type: "path" }
|
126
|
+
end
|
127
|
+
|
128
|
+
return unless package.dig("source", "type") == "git"
|
129
|
+
|
130
|
+
{
|
131
|
+
type: "git",
|
132
|
+
url: package.dig("source", "url")
|
133
|
+
}
|
134
|
+
end
|
135
|
+
|
136
|
+
def lockfile_key(type)
|
137
|
+
case type
|
138
|
+
when "runtime" then "packages"
|
139
|
+
when "development" then "packages-dev"
|
140
|
+
else raise "unknown type #{type}"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def package?(name)
|
145
|
+
# Filter out php, ext-, composer-plugin-api, and other special
|
146
|
+
# packages which don't behave as normal
|
147
|
+
name.split("/").count == 2
|
148
|
+
end
|
149
|
+
|
150
|
+
def check_required_files
|
151
|
+
raise "No composer.json!" unless get_original_file("composer.json")
|
152
|
+
end
|
153
|
+
|
154
|
+
def parsed_lockfile
|
155
|
+
return unless lockfile
|
156
|
+
|
157
|
+
@parsed_lockfile ||= JSON.parse(lockfile.content)
|
158
|
+
rescue JSON::ParserError
|
159
|
+
raise Dependabot::DependencyFileNotParseable, lockfile.path
|
160
|
+
end
|
161
|
+
|
162
|
+
def parsed_composer_json
|
163
|
+
@parsed_composer_json ||= JSON.parse(composer_json.content)
|
164
|
+
rescue JSON::ParserError
|
165
|
+
raise Dependabot::DependencyFileNotParseable, composer_json.path
|
166
|
+
end
|
167
|
+
|
168
|
+
def composer_json
|
169
|
+
@composer_json ||= get_original_file("composer.json")
|
170
|
+
end
|
171
|
+
|
172
|
+
def lockfile
|
173
|
+
@lockfile ||= get_original_file("composer.lock")
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
Dependabot::FileParsers.register("composer", Dependabot::Composer::FileParser)
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dependabot/file_updaters"
|
4
|
+
require "dependabot/file_updaters/base"
|
5
|
+
require "dependabot/shared_helpers"
|
6
|
+
require "dependabot/errors"
|
7
|
+
|
8
|
+
module Dependabot
|
9
|
+
module Composer
|
10
|
+
class FileUpdater < Dependabot::FileUpdaters::Base
|
11
|
+
require_relative "file_updater/manifest_updater"
|
12
|
+
require_relative "file_updater/lockfile_updater"
|
13
|
+
|
14
|
+
def self.updated_files_regex
|
15
|
+
[
|
16
|
+
/^composer\.json$/,
|
17
|
+
/^composer\.lock$/
|
18
|
+
]
|
19
|
+
end
|
20
|
+
|
21
|
+
def updated_dependency_files
|
22
|
+
updated_files = []
|
23
|
+
|
24
|
+
if file_changed?(composer_json)
|
25
|
+
updated_files <<
|
26
|
+
updated_file(
|
27
|
+
file: composer_json,
|
28
|
+
content: updated_composer_json_content
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
if lockfile
|
33
|
+
updated_files <<
|
34
|
+
updated_file(file: lockfile, content: updated_lockfile_content)
|
35
|
+
end
|
36
|
+
|
37
|
+
if updated_files.none? ||
|
38
|
+
updated_files.sort_by(&:name) == dependency_files.sort_by(&:name)
|
39
|
+
raise "No files have changed!"
|
40
|
+
end
|
41
|
+
|
42
|
+
updated_files
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def check_required_files
|
48
|
+
raise "No composer.json!" unless get_original_file("composer.json")
|
49
|
+
end
|
50
|
+
|
51
|
+
def updated_composer_json_content
|
52
|
+
ManifestUpdater.new(
|
53
|
+
dependencies: dependencies,
|
54
|
+
manifest: composer_json
|
55
|
+
).updated_manifest_content
|
56
|
+
end
|
57
|
+
|
58
|
+
def updated_lockfile_content
|
59
|
+
@updated_lockfile_content ||=
|
60
|
+
LockfileUpdater.new(
|
61
|
+
dependencies: dependencies,
|
62
|
+
dependency_files: dependency_files,
|
63
|
+
credentials: credentials
|
64
|
+
).updated_lockfile_content
|
65
|
+
end
|
66
|
+
|
67
|
+
def composer_json
|
68
|
+
@composer_json ||= get_original_file("composer.json")
|
69
|
+
end
|
70
|
+
|
71
|
+
def lockfile
|
72
|
+
@lockfile ||= get_original_file("composer.lock")
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
Dependabot::FileUpdaters.register("composer", Dependabot::Composer::FileUpdater)
|
@@ -0,0 +1,267 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dependabot/shared_helpers"
|
4
|
+
require "dependabot/errors"
|
5
|
+
require "dependabot/composer/file_updater"
|
6
|
+
require "dependabot/composer/version"
|
7
|
+
require "dependabot/composer/native_helpers"
|
8
|
+
|
9
|
+
module Dependabot
|
10
|
+
module Composer
|
11
|
+
class FileUpdater
|
12
|
+
class LockfileUpdater
|
13
|
+
require_relative "manifest_updater"
|
14
|
+
|
15
|
+
def initialize(dependencies:, dependency_files:, credentials:)
|
16
|
+
@dependencies = dependencies
|
17
|
+
@dependency_files = dependency_files
|
18
|
+
@credentials = credentials
|
19
|
+
end
|
20
|
+
|
21
|
+
def updated_lockfile_content
|
22
|
+
base_directory = dependency_files.first.directory
|
23
|
+
@updated_lockfile_content ||=
|
24
|
+
SharedHelpers.in_a_temporary_directory(base_directory) do
|
25
|
+
write_temporary_dependency_files
|
26
|
+
|
27
|
+
updated_content = run_update_helper.fetch("composer.lock")
|
28
|
+
|
29
|
+
updated_content = post_process_lockfile(updated_content)
|
30
|
+
if lockfile.content == updated_content
|
31
|
+
raise "Expected content to change!"
|
32
|
+
end
|
33
|
+
|
34
|
+
updated_content
|
35
|
+
end
|
36
|
+
rescue SharedHelpers::HelperSubprocessFailed => error
|
37
|
+
handle_composer_errors(error)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
attr_reader :dependencies, :dependency_files, :credentials
|
43
|
+
|
44
|
+
def dependency
|
45
|
+
# For now, we'll only ever be updating a single dependency for PHP
|
46
|
+
dependencies.first
|
47
|
+
end
|
48
|
+
|
49
|
+
def run_update_helper
|
50
|
+
SharedHelpers.with_git_configured(credentials: credentials) do
|
51
|
+
SharedHelpers.run_helper_subprocess(
|
52
|
+
command: "php #{php_helper_path}",
|
53
|
+
function: "update",
|
54
|
+
env: credentials_env,
|
55
|
+
args: [
|
56
|
+
Dir.pwd,
|
57
|
+
dependency.name,
|
58
|
+
dependency.version,
|
59
|
+
git_credentials,
|
60
|
+
registry_credentials
|
61
|
+
]
|
62
|
+
)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def updated_composer_json_content
|
67
|
+
ManifestUpdater.new(
|
68
|
+
dependencies: dependencies,
|
69
|
+
manifest: composer_json
|
70
|
+
).updated_manifest_content
|
71
|
+
end
|
72
|
+
|
73
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
74
|
+
# rubocop:disable Metrics/AbcSize
|
75
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
76
|
+
# rubocop:disable Metrics/MethodLength
|
77
|
+
def handle_composer_errors(error)
|
78
|
+
if error.message.start_with?("Failed to execute git checkout")
|
79
|
+
raise git_dependency_reference_error(error)
|
80
|
+
end
|
81
|
+
|
82
|
+
if error.message.start_with?("Failed to execute git clone")
|
83
|
+
dependency_url =
|
84
|
+
error.message.match(/(?:mirror|checkout) '(?<url>.*?)'/).
|
85
|
+
named_captures.fetch("url")
|
86
|
+
raise GitDependenciesNotReachable, dependency_url
|
87
|
+
end
|
88
|
+
|
89
|
+
if error.message.start_with?("Failed to clone")
|
90
|
+
dependency_url =
|
91
|
+
error.message.match(/Failed to clone (?<url>.*?) via/).
|
92
|
+
named_captures.fetch("url")
|
93
|
+
raise GitDependenciesNotReachable, dependency_url
|
94
|
+
end
|
95
|
+
|
96
|
+
if error.message.start_with?("Could not find a key for ACF PRO")
|
97
|
+
raise MissingEnvironmentVariable, "ACF_PRO_KEY"
|
98
|
+
end
|
99
|
+
|
100
|
+
if error.message.start_with?("Unknown downloader type: npm-sign") ||
|
101
|
+
error.message.include?("file could not be downloaded") ||
|
102
|
+
error.message.include?("configuration does not allow connect")
|
103
|
+
raise DependencyFileNotResolvable, error.message
|
104
|
+
end
|
105
|
+
|
106
|
+
if error.message.start_with?("Allowed memory size")
|
107
|
+
raise Dependabot::OutOfMemory
|
108
|
+
end
|
109
|
+
|
110
|
+
if error.message.include?("403 Forbidden")
|
111
|
+
source = error.message.match(%r{https?://(?<source>[^/]+)/}).
|
112
|
+
named_captures.fetch("source")
|
113
|
+
raise PrivateSourceAuthenticationFailure, source
|
114
|
+
end
|
115
|
+
|
116
|
+
if error.message.include?("Argument 1 passed to Composer")
|
117
|
+
msg = "One of your Composer plugins is not compatible with the "\
|
118
|
+
"latest version of Composer. Please update Composer and "\
|
119
|
+
"try running `composer update` to debug further."
|
120
|
+
raise DependencyFileNotResolvable, msg
|
121
|
+
end
|
122
|
+
|
123
|
+
raise error
|
124
|
+
end
|
125
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
126
|
+
# rubocop:enable Metrics/AbcSize
|
127
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
128
|
+
# rubocop:enable Metrics/MethodLength
|
129
|
+
|
130
|
+
def write_temporary_dependency_files
|
131
|
+
path_dependencies.each do |file|
|
132
|
+
path = file.name
|
133
|
+
FileUtils.mkdir_p(Pathname.new(path).dirname)
|
134
|
+
File.write(file.name, file.content)
|
135
|
+
end
|
136
|
+
|
137
|
+
File.write("composer.json", locked_composer_json_content)
|
138
|
+
File.write("composer.lock", lockfile.content)
|
139
|
+
end
|
140
|
+
|
141
|
+
def locked_composer_json_content
|
142
|
+
dependencies.
|
143
|
+
reduce(updated_composer_json_content) do |content, dep|
|
144
|
+
updated_req = dep.version
|
145
|
+
next content unless Composer::Version.correct?(updated_req)
|
146
|
+
|
147
|
+
old_req =
|
148
|
+
dep.requirements.find { |r| r[:file] == "composer.json" }&.
|
149
|
+
fetch(:requirement)
|
150
|
+
|
151
|
+
# When updating a subdep there won't be an old requirement
|
152
|
+
next content unless old_req
|
153
|
+
|
154
|
+
regex =
|
155
|
+
/
|
156
|
+
"#{Regexp.escape(dep.name)}"\s*:\s*
|
157
|
+
"#{Regexp.escape(old_req)}"
|
158
|
+
/x
|
159
|
+
|
160
|
+
content.gsub(regex) do |declaration|
|
161
|
+
declaration.gsub(%("#{old_req}"), %("#{updated_req}"))
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def git_dependency_reference_error(error)
|
167
|
+
ref = error.message.match(/checkout '(?<ref>.*?)'/).
|
168
|
+
named_captures.fetch("ref")
|
169
|
+
dependency_name =
|
170
|
+
JSON.parse(lockfile.content).
|
171
|
+
values_at("packages", "packages-dev").flatten(1).
|
172
|
+
find { |dep| dep.dig("source", "reference") == ref }&.
|
173
|
+
fetch("name")
|
174
|
+
|
175
|
+
raise unless dependency_name
|
176
|
+
|
177
|
+
raise GitDependencyReferenceNotFound, dependency_name
|
178
|
+
end
|
179
|
+
|
180
|
+
def post_process_lockfile(content)
|
181
|
+
content = replace_patches(content)
|
182
|
+
replace_content_hash(content)
|
183
|
+
end
|
184
|
+
|
185
|
+
def replace_patches(updated_content)
|
186
|
+
content = updated_content
|
187
|
+
%w(packages packages-dev).each do |package_type|
|
188
|
+
JSON.parse(lockfile.content).fetch(package_type).each do |details|
|
189
|
+
next unless details["extra"].is_a?(Hash)
|
190
|
+
next unless (patches = details.dig("extra", "patches_applied"))
|
191
|
+
|
192
|
+
updated_object = JSON.parse(content)
|
193
|
+
updated_object_package =
|
194
|
+
updated_object.
|
195
|
+
fetch(package_type).
|
196
|
+
find { |d| d["name"] == details["name"] }
|
197
|
+
|
198
|
+
next unless updated_object_package
|
199
|
+
|
200
|
+
updated_object_package["extra"] ||= {}
|
201
|
+
updated_object_package["extra"]["patches_applied"] = patches
|
202
|
+
|
203
|
+
content =
|
204
|
+
JSON.pretty_generate(updated_object, indent: " ").
|
205
|
+
gsub(/\[\n\n\s*\]/, "[]").
|
206
|
+
gsub(/\}\z/, "}\n")
|
207
|
+
end
|
208
|
+
end
|
209
|
+
content
|
210
|
+
end
|
211
|
+
|
212
|
+
def replace_content_hash(content)
|
213
|
+
existing_hash = JSON.parse(content).fetch("content-hash")
|
214
|
+
SharedHelpers.in_a_temporary_directory do
|
215
|
+
File.write("composer.json", updated_composer_json_content)
|
216
|
+
|
217
|
+
content_hash =
|
218
|
+
SharedHelpers.run_helper_subprocess(
|
219
|
+
command: "php #{php_helper_path}",
|
220
|
+
function: "get_content_hash",
|
221
|
+
env: credentials_env,
|
222
|
+
args: [Dir.pwd]
|
223
|
+
)
|
224
|
+
|
225
|
+
content.gsub(existing_hash, content_hash)
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
def php_helper_path
|
230
|
+
NativeHelpers.composer_helper_path
|
231
|
+
end
|
232
|
+
|
233
|
+
def credentials_env
|
234
|
+
credentials.
|
235
|
+
select { |c| c.fetch("type") == "php_environment_variable" }.
|
236
|
+
map { |cred| [cred["env-key"], cred["env-value"]] }.
|
237
|
+
to_h
|
238
|
+
end
|
239
|
+
|
240
|
+
def git_credentials
|
241
|
+
credentials.
|
242
|
+
select { |cred| cred.fetch("type") == "git_source" }
|
243
|
+
end
|
244
|
+
|
245
|
+
def registry_credentials
|
246
|
+
credentials.
|
247
|
+
select { |cred| cred.fetch("type") == "composer_repository" }
|
248
|
+
end
|
249
|
+
|
250
|
+
def composer_json
|
251
|
+
@composer_json ||=
|
252
|
+
dependency_files.find { |f| f.name == "composer.json" }
|
253
|
+
end
|
254
|
+
|
255
|
+
def lockfile
|
256
|
+
@lockfile ||=
|
257
|
+
dependency_files.find { |f| f.name == "composer.lock" }
|
258
|
+
end
|
259
|
+
|
260
|
+
def path_dependencies
|
261
|
+
@path_dependencies ||=
|
262
|
+
dependency_files.select { |f| f.name.end_with?("/composer.json") }
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|