dependabot-composer 0.89.0
Sign up to get free protection for your applications and to get access to all the features.
- 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,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dependabot/composer/file_updater"
|
4
|
+
|
5
|
+
module Dependabot
|
6
|
+
module Composer
|
7
|
+
class FileUpdater
|
8
|
+
class ManifestUpdater
|
9
|
+
def initialize(dependencies:, manifest:)
|
10
|
+
@dependencies = dependencies
|
11
|
+
@manifest = manifest
|
12
|
+
end
|
13
|
+
|
14
|
+
def updated_manifest_content
|
15
|
+
dependencies.reduce(manifest.content.dup) do |content, dep|
|
16
|
+
updated_content = content
|
17
|
+
updated_requirements(dep).each do |new_req|
|
18
|
+
old_req = old_requirement(dep, new_req).fetch(:requirement)
|
19
|
+
updated_req = new_req.fetch(:requirement)
|
20
|
+
|
21
|
+
regex =
|
22
|
+
/
|
23
|
+
"#{Regexp.escape(dep.name)}"\s*:\s*
|
24
|
+
"#{Regexp.escape(old_req)}"
|
25
|
+
/x
|
26
|
+
|
27
|
+
updated_content = content.gsub(regex) do |declaration|
|
28
|
+
declaration.gsub(%("#{old_req}"), %("#{updated_req}"))
|
29
|
+
end
|
30
|
+
|
31
|
+
raise "Expected content to change!" if content == updated_content
|
32
|
+
end
|
33
|
+
|
34
|
+
updated_content
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
attr_reader :dependencies, :manifest
|
41
|
+
|
42
|
+
def new_requirements(dependency)
|
43
|
+
dependency.requirements.select { |r| r[:file] == manifest.name }
|
44
|
+
end
|
45
|
+
|
46
|
+
def old_requirement(dependency, new_requirement)
|
47
|
+
dependency.previous_requirements.
|
48
|
+
select { |r| r[:file] == manifest.name }.
|
49
|
+
find { |r| r[:groups] == new_requirement[:groups] }
|
50
|
+
end
|
51
|
+
|
52
|
+
def updated_requirements(dependency)
|
53
|
+
new_requirements(dependency).
|
54
|
+
reject { |r| dependency.previous_requirements.include?(r) }
|
55
|
+
end
|
56
|
+
|
57
|
+
def requirement_changed?(file, dependency)
|
58
|
+
changed_requirements =
|
59
|
+
dependency.requirements - dependency.previous_requirements
|
60
|
+
|
61
|
+
changed_requirements.any? { |f| f[:file] == file.name }
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "excon"
|
4
|
+
require "dependabot/metadata_finders"
|
5
|
+
require "dependabot/metadata_finders/base"
|
6
|
+
require "dependabot/shared_helpers"
|
7
|
+
require "dependabot/composer/version"
|
8
|
+
|
9
|
+
module Dependabot
|
10
|
+
module Composer
|
11
|
+
class MetadataFinder < Dependabot::MetadataFinders::Base
|
12
|
+
private
|
13
|
+
|
14
|
+
def look_up_source
|
15
|
+
source_from_dependency || look_up_source_from_packagist
|
16
|
+
end
|
17
|
+
|
18
|
+
def source_from_dependency
|
19
|
+
source_url =
|
20
|
+
dependency.requirements.
|
21
|
+
map { |r| r.fetch(:source) }.compact.
|
22
|
+
first&.fetch(:url, nil)
|
23
|
+
|
24
|
+
Source.from_url(source_url)
|
25
|
+
end
|
26
|
+
|
27
|
+
def look_up_source_from_packagist
|
28
|
+
return nil if packagist_listing&.fetch("packages", nil) == []
|
29
|
+
unless packagist_listing&.dig("packages", dependency.name.downcase)
|
30
|
+
return nil
|
31
|
+
end
|
32
|
+
|
33
|
+
version_listings =
|
34
|
+
packagist_listing["packages"][dependency.name.downcase].
|
35
|
+
select { |version, _| Composer::Version.correct?(version) }.
|
36
|
+
sort_by { |version, _| Composer::Version.new(version) }.
|
37
|
+
map { |_, listing| listing }.
|
38
|
+
reverse
|
39
|
+
|
40
|
+
potential_source_urls =
|
41
|
+
version_listings.
|
42
|
+
flat_map { |info| [info["homepage"], info.dig("source", "url")] }.
|
43
|
+
compact
|
44
|
+
|
45
|
+
source_url = potential_source_urls.find { |url| Source.from_url(url) }
|
46
|
+
|
47
|
+
Source.from_url(source_url)
|
48
|
+
end
|
49
|
+
|
50
|
+
def packagist_listing
|
51
|
+
return @packagist_listing unless @packagist_listing.nil?
|
52
|
+
|
53
|
+
response = Excon.get(
|
54
|
+
"https://packagist.org/p/#{dependency.name.downcase}.json",
|
55
|
+
idempotent: true,
|
56
|
+
**SharedHelpers.excon_defaults
|
57
|
+
)
|
58
|
+
|
59
|
+
return nil unless response.status == 200
|
60
|
+
|
61
|
+
@packagist_listing = JSON.parse(response.body)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
Dependabot::MetadataFinders.
|
68
|
+
register("composer", Dependabot::Composer::MetadataFinder)
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dependabot
|
4
|
+
module Composer
|
5
|
+
module NativeHelpers
|
6
|
+
def self.composer_helper_path
|
7
|
+
File.join(composer_helpers_dir, "bin/run.php")
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.composer_helpers_dir
|
11
|
+
File.join(native_helpers_root, "composer/helpers")
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.native_helpers_root
|
15
|
+
default_path = File.join(__dir__, "../../../..")
|
16
|
+
ENV.fetch("DEPENDABOT_NATIVE_HELPERS_PATH", default_path)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dependabot/utils"
|
4
|
+
|
5
|
+
module Dependabot
|
6
|
+
module Composer
|
7
|
+
class Requirement < Gem::Requirement
|
8
|
+
AND_SEPARATOR =
|
9
|
+
/(?<=[a-zA-Z0-9*])(?<!\sas)[\s,]+(?![\s,]*[|-]|as)/.freeze
|
10
|
+
OR_SEPARATOR = /(?<=[a-zA-Z0-9*])[\s,]*\|\|?\s*/.freeze
|
11
|
+
|
12
|
+
def self.parse(obj)
|
13
|
+
new_obj = obj.gsub(/@\w+/, "").gsub(/[a-z0-9\-_\.]*\sas\s+/i, "")
|
14
|
+
super(new_obj)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Returns an array of requirements. At least one requirement from the
|
18
|
+
# returned array must be satisfied for a version to be valid.
|
19
|
+
def self.requirements_array(requirement_string)
|
20
|
+
requirement_string.strip.split(OR_SEPARATOR).map do |req_string|
|
21
|
+
new(req_string)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(*requirements)
|
26
|
+
requirements =
|
27
|
+
requirements.flatten.
|
28
|
+
flat_map { |req_string| req_string.split(AND_SEPARATOR) }.
|
29
|
+
flat_map { |req| convert_php_constraint_to_ruby_constraint(req) }
|
30
|
+
|
31
|
+
super(requirements)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
37
|
+
def convert_php_constraint_to_ruby_constraint(req_string)
|
38
|
+
req_string = req_string.gsub(/v(?=\d)/, "")
|
39
|
+
|
40
|
+
# Return an unlikely version if a dev requirement is specified. This
|
41
|
+
# ensures that the dev-requirement doesn't match anything.
|
42
|
+
return "0-dev-branch-match" if req_string.strip.start_with?("dev-")
|
43
|
+
|
44
|
+
if req_string.start_with?("*") then ">= 0"
|
45
|
+
elsif req_string.include?("*") then convert_wildcard_req(req_string)
|
46
|
+
elsif req_string.match?(/^~[^>]/) then convert_tilde_req(req_string)
|
47
|
+
elsif req_string.start_with?("^") then convert_caret_req(req_string)
|
48
|
+
elsif req_string.match?(/\s-\s/) then convert_hyphen_req(req_string)
|
49
|
+
else req_string
|
50
|
+
end
|
51
|
+
end
|
52
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
53
|
+
|
54
|
+
def convert_wildcard_req(req_string)
|
55
|
+
version = req_string.gsub(/^~/, "").gsub(/(?:\.|^)\*/, "")
|
56
|
+
"~> #{version}.0"
|
57
|
+
end
|
58
|
+
|
59
|
+
def convert_tilde_req(req_string)
|
60
|
+
version = req_string.gsub(/^~/, "")
|
61
|
+
"~> #{version}"
|
62
|
+
end
|
63
|
+
|
64
|
+
def convert_caret_req(req_string)
|
65
|
+
version = req_string.gsub(/^\^/, "")
|
66
|
+
parts = version.split(".")
|
67
|
+
first_non_zero = parts.find { |d| d != "0" }
|
68
|
+
first_non_zero_index =
|
69
|
+
first_non_zero ? parts.index(first_non_zero) : parts.count - 1
|
70
|
+
upper_bound = parts.map.with_index do |part, i|
|
71
|
+
if i < first_non_zero_index then part
|
72
|
+
elsif i == first_non_zero_index then (part.to_i + 1).to_s
|
73
|
+
else 0
|
74
|
+
end
|
75
|
+
end.join(".")
|
76
|
+
|
77
|
+
[">= #{version}", "< #{upper_bound}"]
|
78
|
+
end
|
79
|
+
|
80
|
+
def convert_hyphen_req(req_string)
|
81
|
+
req_string = req_string
|
82
|
+
lower_bound, upper_bound = req_string.split(/\s+-\s+/)
|
83
|
+
if upper_bound.split(".").count < 3
|
84
|
+
upper_bound_parts = upper_bound.split(".")
|
85
|
+
upper_bound_parts[-1] = (upper_bound_parts[-1].to_i + 1).to_s
|
86
|
+
upper_bound = upper_bound_parts.join(".")
|
87
|
+
|
88
|
+
[">= #{lower_bound}", "< #{upper_bound}"]
|
89
|
+
else
|
90
|
+
[">= #{lower_bound}", "<= #{upper_bound}"]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
Dependabot::Utils.
|
98
|
+
register_requirement_class("composer", Dependabot::Composer::Requirement)
|
@@ -0,0 +1,176 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "excon"
|
4
|
+
require "json"
|
5
|
+
require "dependabot/update_checkers"
|
6
|
+
require "dependabot/update_checkers/base"
|
7
|
+
require "dependabot/shared_helpers"
|
8
|
+
require "dependabot/errors"
|
9
|
+
|
10
|
+
module Dependabot
|
11
|
+
module Composer
|
12
|
+
class UpdateChecker < Dependabot::UpdateCheckers::Base
|
13
|
+
require_relative "update_checker/requirements_updater"
|
14
|
+
require_relative "update_checker/version_resolver"
|
15
|
+
|
16
|
+
def latest_version
|
17
|
+
return nil if path_dependency?
|
18
|
+
|
19
|
+
# Fall back to latest_resolvable_version if no listings found
|
20
|
+
latest_version_from_registry || latest_resolvable_version
|
21
|
+
end
|
22
|
+
|
23
|
+
def latest_resolvable_version
|
24
|
+
return nil if path_dependency?
|
25
|
+
|
26
|
+
@latest_resolvable_version ||=
|
27
|
+
VersionResolver.new(
|
28
|
+
credentials: credentials,
|
29
|
+
dependency: dependency,
|
30
|
+
dependency_files: dependency_files,
|
31
|
+
latest_allowable_version: latest_version_from_registry,
|
32
|
+
requirements_to_unlock: :own
|
33
|
+
).latest_resolvable_version
|
34
|
+
end
|
35
|
+
|
36
|
+
def latest_resolvable_version_with_no_unlock
|
37
|
+
return nil if path_dependency?
|
38
|
+
|
39
|
+
@latest_resolvable_version_with_no_unlock ||=
|
40
|
+
VersionResolver.new(
|
41
|
+
credentials: credentials,
|
42
|
+
dependency: dependency,
|
43
|
+
dependency_files: dependency_files,
|
44
|
+
latest_allowable_version: latest_version_from_registry,
|
45
|
+
requirements_to_unlock: :none
|
46
|
+
).latest_resolvable_version
|
47
|
+
end
|
48
|
+
|
49
|
+
def updated_requirements
|
50
|
+
RequirementsUpdater.new(
|
51
|
+
requirements: dependency.requirements,
|
52
|
+
latest_version: latest_version&.to_s,
|
53
|
+
latest_resolvable_version: latest_resolvable_version&.to_s,
|
54
|
+
update_strategy: requirements_update_strategy
|
55
|
+
).updated_requirements
|
56
|
+
end
|
57
|
+
|
58
|
+
def requirements_update_strategy
|
59
|
+
# If passed in as an option (in the base class) honour that option
|
60
|
+
if @requirements_update_strategy
|
61
|
+
return @requirements_update_strategy.to_sym
|
62
|
+
end
|
63
|
+
|
64
|
+
# Otherwise, widen ranges for libraries and bump versions for apps
|
65
|
+
library? ? :widen_ranges : :bump_versions_if_necessary
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def latest_version_resolvable_with_full_unlock?
|
71
|
+
# Full unlock checks aren't implemented for Composer (yet)
|
72
|
+
false
|
73
|
+
end
|
74
|
+
|
75
|
+
def latest_version_from_registry
|
76
|
+
versions =
|
77
|
+
registry_versions.
|
78
|
+
select { |version| version_class.correct?(version.gsub(/^v/, "")) }.
|
79
|
+
map { |version| version_class.new(version.gsub(/^v/, "")) }
|
80
|
+
|
81
|
+
versions.reject!(&:prerelease?) unless wants_prerelease?
|
82
|
+
versions.reject! { |v| ignore_reqs.any? { |r| r.satisfied_by?(v) } }
|
83
|
+
versions.max
|
84
|
+
end
|
85
|
+
|
86
|
+
def wants_prerelease?
|
87
|
+
current_version = dependency.version
|
88
|
+
if current_version && version_class.new(current_version).prerelease?
|
89
|
+
return true
|
90
|
+
end
|
91
|
+
|
92
|
+
dependency.requirements.any? do |req|
|
93
|
+
req[:requirement].match?(/\d-[A-Za-z]/)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def updated_dependencies_after_full_unlock
|
98
|
+
raise NotImplementedError
|
99
|
+
end
|
100
|
+
|
101
|
+
def path_dependency?
|
102
|
+
dependency.requirements.any? { |r| r.dig(:source, :type) == "path" }
|
103
|
+
end
|
104
|
+
|
105
|
+
def composer_file
|
106
|
+
composer_file =
|
107
|
+
dependency_files.find { |f| f.name == "composer.json" }
|
108
|
+
raise "No composer.json!" unless composer_file
|
109
|
+
|
110
|
+
composer_file
|
111
|
+
end
|
112
|
+
|
113
|
+
def lockfile
|
114
|
+
dependency_files.find { |f| f.name == "composer.lock" }
|
115
|
+
end
|
116
|
+
|
117
|
+
def registry_versions
|
118
|
+
return @registry_versions unless @registry_versions.nil?
|
119
|
+
|
120
|
+
repositories =
|
121
|
+
JSON.parse(composer_file.content).
|
122
|
+
fetch("repositories", []).
|
123
|
+
select { |r| r.is_a?(Hash) }
|
124
|
+
|
125
|
+
urls = repositories.
|
126
|
+
select { |h| h["type"] == "composer" }.
|
127
|
+
map { |h| h["url"] }.compact.
|
128
|
+
map { |url| url.gsub(%r{\/$}, "") + "/packages.json" }
|
129
|
+
|
130
|
+
unless repositories.any? { |rep| rep["packagist.org"] == false }
|
131
|
+
urls << "https://packagist.org/p/#{dependency.name.downcase}.json"
|
132
|
+
end
|
133
|
+
|
134
|
+
@registry_versions = []
|
135
|
+
urls.each do |url|
|
136
|
+
@registry_versions += fetch_registry_versions_from_url(url)
|
137
|
+
end
|
138
|
+
@registry_versions.uniq
|
139
|
+
end
|
140
|
+
|
141
|
+
def fetch_registry_versions_from_url(url)
|
142
|
+
cred = registry_credentials.find { |c| url.include?(c["registry"]) }
|
143
|
+
|
144
|
+
response = Excon.get(
|
145
|
+
url,
|
146
|
+
idempotent: true,
|
147
|
+
user: cred&.fetch("username", nil),
|
148
|
+
password: cred&.fetch("password", nil),
|
149
|
+
**SharedHelpers.excon_defaults
|
150
|
+
)
|
151
|
+
|
152
|
+
return [] unless response.status == 200
|
153
|
+
|
154
|
+
listing = JSON.parse(response.body)
|
155
|
+
return [] if listing.nil?
|
156
|
+
return [] if listing.fetch("packages", []) == []
|
157
|
+
return [] unless listing.dig("packages", dependency.name.downcase)
|
158
|
+
|
159
|
+
listing.dig("packages", dependency.name.downcase).keys
|
160
|
+
rescue Excon::Error::Socket, Excon::Error::Timeout
|
161
|
+
[]
|
162
|
+
end
|
163
|
+
|
164
|
+
def library?
|
165
|
+
JSON.parse(composer_file.content)["type"] == "library"
|
166
|
+
end
|
167
|
+
|
168
|
+
def registry_credentials
|
169
|
+
credentials.select { |cred| cred["type"] == "composer_repository" }
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
Dependabot::UpdateCheckers.
|
176
|
+
register("composer", Dependabot::Composer::UpdateChecker)
|
@@ -0,0 +1,253 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
################################################################################
|
4
|
+
# For more details on Composer version constraints, see: #
|
5
|
+
# https://getcomposer.org/doc/articles/versions.md#writing-version-constraints #
|
6
|
+
################################################################################
|
7
|
+
|
8
|
+
require "dependabot/composer/update_checker"
|
9
|
+
require "dependabot/composer/version"
|
10
|
+
require "dependabot/composer/requirement"
|
11
|
+
|
12
|
+
module Dependabot
|
13
|
+
module Composer
|
14
|
+
class UpdateChecker
|
15
|
+
class RequirementsUpdater
|
16
|
+
ALIAS_REGEX = /[a-z0-9\-_\.]*\sas\s+/.freeze
|
17
|
+
VERSION_REGEX =
|
18
|
+
/(?:#{ALIAS_REGEX})?[0-9]+(?:\.[a-zA-Z0-9*\-]+)*/.freeze
|
19
|
+
AND_SEPARATOR =
|
20
|
+
/(?<=[a-zA-Z0-9*])(?<!\sas)[\s,]+(?![\s,]*[|-]|as)/.freeze
|
21
|
+
OR_SEPARATOR = /(?<=[a-zA-Z0-9*])[\s,]*\|\|?\s*/.freeze
|
22
|
+
SEPARATOR = /(?:#{AND_SEPARATOR})|(?:#{OR_SEPARATOR})/.freeze
|
23
|
+
ALLOWED_UPDATE_STRATEGIES =
|
24
|
+
%i(widen_ranges bump_versions bump_versions_if_necessary).freeze
|
25
|
+
|
26
|
+
def initialize(requirements:, update_strategy:,
|
27
|
+
latest_version:, latest_resolvable_version:)
|
28
|
+
@requirements = requirements
|
29
|
+
@update_strategy = update_strategy
|
30
|
+
|
31
|
+
check_update_strategy
|
32
|
+
|
33
|
+
@latest_version = version_class.new(latest_version) if latest_version
|
34
|
+
return unless latest_resolvable_version
|
35
|
+
|
36
|
+
@latest_resolvable_version =
|
37
|
+
version_class.new(latest_resolvable_version)
|
38
|
+
end
|
39
|
+
|
40
|
+
def updated_requirements
|
41
|
+
return requirements unless latest_resolvable_version
|
42
|
+
|
43
|
+
requirements.map { |req| updated_requirement(req) }
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
attr_reader :requirements, :update_strategy,
|
49
|
+
:latest_version, :latest_resolvable_version
|
50
|
+
|
51
|
+
def check_update_strategy
|
52
|
+
return if ALLOWED_UPDATE_STRATEGIES.include?(update_strategy)
|
53
|
+
|
54
|
+
raise "Unknown update strategy: #{update_strategy}"
|
55
|
+
end
|
56
|
+
|
57
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
58
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
59
|
+
def updated_requirement(req)
|
60
|
+
req_string = req[:requirement].strip
|
61
|
+
or_string_reqs = req_string.split(OR_SEPARATOR)
|
62
|
+
or_separator = req_string.match(OR_SEPARATOR)&.to_s || " || "
|
63
|
+
numeric_or_string_reqs = or_string_reqs.
|
64
|
+
reject { |r| r.start_with?("dev-") }
|
65
|
+
branch_or_string_reqs = or_string_reqs.
|
66
|
+
select { |r| r.start_with?("dev-") }
|
67
|
+
|
68
|
+
return req unless req_string.match?(/\d/)
|
69
|
+
return req if numeric_or_string_reqs.none?
|
70
|
+
return updated_alias(req) if req_string.match?(ALIAS_REGEX)
|
71
|
+
return req if req_satisfied_by_latest_resolvable?(req_string) &&
|
72
|
+
update_strategy != :bump_versions
|
73
|
+
|
74
|
+
new_req =
|
75
|
+
case update_strategy
|
76
|
+
when :widen_ranges
|
77
|
+
widen_requirement(req, or_separator)
|
78
|
+
when :bump_versions, :bump_versions_if_necessary
|
79
|
+
update_requirement_version(req, or_separator)
|
80
|
+
end
|
81
|
+
|
82
|
+
new_req_string =
|
83
|
+
[new_req[:requirement], *branch_or_string_reqs].join(or_separator)
|
84
|
+
new_req.merge(requirement: new_req_string)
|
85
|
+
end
|
86
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
87
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
88
|
+
|
89
|
+
def updated_alias(req)
|
90
|
+
req_string = req[:requirement]
|
91
|
+
real_version = req_string.split(/\sas\s/).first.strip
|
92
|
+
|
93
|
+
# If the version we're aliasing isn't a version then we don't know
|
94
|
+
# how to update it, so we just return the existing requirement.
|
95
|
+
return req unless version_class.correct?(real_version)
|
96
|
+
|
97
|
+
new_version_string = latest_resolvable_version.to_s
|
98
|
+
new_req = req_string.sub(real_version, new_version_string)
|
99
|
+
req.merge(requirement: new_req)
|
100
|
+
end
|
101
|
+
|
102
|
+
def widen_requirement(req, or_separator)
|
103
|
+
current_requirement = req[:requirement]
|
104
|
+
reqs = current_requirement.strip.split(SEPARATOR).map(&:strip)
|
105
|
+
|
106
|
+
updated_requirement =
|
107
|
+
if reqs.any? { |r| r.start_with?("^") }
|
108
|
+
update_caret_requirement(current_requirement, or_separator)
|
109
|
+
elsif reqs.any? { |r| r.start_with?("~") }
|
110
|
+
update_tilda_requirement(current_requirement, or_separator)
|
111
|
+
elsif reqs.any? { |r| r.include?("*") }
|
112
|
+
update_wildcard_requirement(current_requirement, or_separator)
|
113
|
+
elsif reqs.any? { |r| r.match?(/<|(\s+-\s+)/) }
|
114
|
+
update_range_requirement(current_requirement, or_separator)
|
115
|
+
else
|
116
|
+
update_version_string(current_requirement)
|
117
|
+
end
|
118
|
+
|
119
|
+
req.merge(requirement: updated_requirement)
|
120
|
+
end
|
121
|
+
|
122
|
+
def update_requirement_version(req, or_separator)
|
123
|
+
current_requirement = req[:requirement]
|
124
|
+
reqs = current_requirement.strip.split(SEPARATOR).map(&:strip)
|
125
|
+
|
126
|
+
updated_requirement =
|
127
|
+
if reqs.count > 1
|
128
|
+
"^#{latest_resolvable_version}"
|
129
|
+
elsif reqs.any? { |r| r.match?(/<|(\s+-\s+)/) }
|
130
|
+
update_range_requirement(current_requirement, or_separator)
|
131
|
+
elsif reqs.any? { |r| r.match?(/>[^=]/) }
|
132
|
+
current_requirement
|
133
|
+
else
|
134
|
+
update_version_string(current_requirement)
|
135
|
+
end
|
136
|
+
|
137
|
+
req.merge(requirement: updated_requirement)
|
138
|
+
end
|
139
|
+
|
140
|
+
def req_satisfied_by_latest_resolvable?(requirement_string)
|
141
|
+
ruby_requirements(requirement_string).
|
142
|
+
any? { |r| r.satisfied_by?(latest_resolvable_version) }
|
143
|
+
end
|
144
|
+
|
145
|
+
def update_version_string(req_string)
|
146
|
+
req_string.
|
147
|
+
sub(VERSION_REGEX) do |old_version|
|
148
|
+
unless req_string.match?(/[~*\^]/)
|
149
|
+
next latest_resolvable_version.to_s
|
150
|
+
end
|
151
|
+
|
152
|
+
old_parts = old_version.split(".")
|
153
|
+
new_parts = latest_resolvable_version.to_s.split(".").
|
154
|
+
first(old_parts.count)
|
155
|
+
new_parts.map.with_index do |part, i|
|
156
|
+
old_parts[i] == "*" ? "*" : part
|
157
|
+
end.join(".")
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def ruby_requirements(requirement_string)
|
162
|
+
Composer::Requirement.requirements_array(requirement_string)
|
163
|
+
end
|
164
|
+
|
165
|
+
def update_caret_requirement(req_string, or_separator)
|
166
|
+
caret_requirements =
|
167
|
+
req_string.split(SEPARATOR).select { |r| r.start_with?("^") }
|
168
|
+
version_parts = latest_resolvable_version.segments
|
169
|
+
|
170
|
+
min_existing_precision =
|
171
|
+
caret_requirements.map { |r| r.split(".").count }.min
|
172
|
+
first_non_zero_index =
|
173
|
+
version_parts.count.times.find { |i| version_parts[i] != 0 }
|
174
|
+
|
175
|
+
precision = [min_existing_precision, first_non_zero_index + 1].max
|
176
|
+
version = version_parts.first(precision).map.with_index do |part, i|
|
177
|
+
i <= first_non_zero_index ? part : 0
|
178
|
+
end.join(".")
|
179
|
+
|
180
|
+
req_string + "#{or_separator}^#{version}"
|
181
|
+
end
|
182
|
+
|
183
|
+
def update_tilda_requirement(req_string, or_separator)
|
184
|
+
tilda_requirements =
|
185
|
+
req_string.split(SEPARATOR).select { |r| r.start_with?("~") }
|
186
|
+
precision = tilda_requirements.map { |r| r.split(".").count }.min
|
187
|
+
|
188
|
+
version_parts = latest_resolvable_version.segments.first(precision)
|
189
|
+
version_parts[-1] = 0
|
190
|
+
version = version_parts.join(".")
|
191
|
+
|
192
|
+
req_string + "#{or_separator}~#{version}"
|
193
|
+
end
|
194
|
+
|
195
|
+
def update_wildcard_requirement(req_string, or_separator)
|
196
|
+
wildcard_requirements =
|
197
|
+
req_string.split(SEPARATOR).select { |r| r.include?("*") }
|
198
|
+
precision = wildcard_requirements.map do |r|
|
199
|
+
r.split(".").reject { |s| s == "*" }.count
|
200
|
+
end.min
|
201
|
+
wildcard_count = wildcard_requirements.map do |r|
|
202
|
+
r.split(".").select { |s| s == "*" }.count
|
203
|
+
end.min
|
204
|
+
|
205
|
+
version_parts = latest_resolvable_version.segments.first(precision)
|
206
|
+
version = version_parts.join(".")
|
207
|
+
|
208
|
+
req_string + "#{or_separator}#{version}#{'.*' * wildcard_count}"
|
209
|
+
end
|
210
|
+
|
211
|
+
def update_range_requirement(req_string, or_separator)
|
212
|
+
range_requirements =
|
213
|
+
req_string.split(SEPARATOR).select { |r| r.match?(/<|(\s+-\s+)/) }
|
214
|
+
|
215
|
+
if range_requirements.count == 1
|
216
|
+
range_requirement = range_requirements.first
|
217
|
+
versions = range_requirement.scan(VERSION_REGEX)
|
218
|
+
upper_bound = versions.map { |v| version_class.new(v) }.max
|
219
|
+
new_upper_bound = update_greatest_version(
|
220
|
+
upper_bound,
|
221
|
+
latest_resolvable_version
|
222
|
+
)
|
223
|
+
|
224
|
+
req_string.sub(upper_bound.to_s, new_upper_bound.to_s)
|
225
|
+
else
|
226
|
+
req_string + "#{or_separator}^#{latest_resolvable_version}"
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def update_greatest_version(old_version, version_to_be_permitted)
|
231
|
+
version = version_class.new(old_version)
|
232
|
+
version = version.release if version.prerelease?
|
233
|
+
|
234
|
+
index_to_update =
|
235
|
+
version.segments.map.with_index { |seg, i| seg.zero? ? 0 : i }.max
|
236
|
+
|
237
|
+
version.segments.map.with_index do |_, index|
|
238
|
+
if index < index_to_update
|
239
|
+
version_to_be_permitted.segments[index]
|
240
|
+
elsif index == index_to_update
|
241
|
+
version_to_be_permitted.segments[index] + 1
|
242
|
+
else 0
|
243
|
+
end
|
244
|
+
end.join(".")
|
245
|
+
end
|
246
|
+
|
247
|
+
def version_class
|
248
|
+
Composer::Version
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|