dependabot-bundler 0.129.1 → 0.130.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 +4 -4
- data/helpers/build +18 -0
- data/helpers/lib/functions.rb +193 -0
- data/helpers/lib/functions/conflicting_dependency_resolver.rb +86 -0
- data/helpers/lib/functions/dependency_source.rb +86 -0
- data/helpers/lib/functions/file_parser.rb +105 -0
- data/helpers/lib/functions/force_updater.rb +167 -0
- data/helpers/lib/functions/lockfile_updater.rb +224 -0
- data/helpers/lib/functions/version_resolver.rb +140 -0
- data/helpers/monkey_patches/definition_bundler_version_patch.rb +15 -0
- data/helpers/monkey_patches/definition_ruby_version_patch.rb +20 -0
- data/helpers/monkey_patches/git_source_patch.rb +63 -0
- data/helpers/run.rb +30 -0
- data/lib/dependabot/bundler/file_updater/ruby_requirement_setter.rb +6 -3
- metadata +20 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a13db3fcb8629f8472eb78a6e9120a0fa557cb08c95f6f4009bbeab84c6c6e18
|
4
|
+
data.tar.gz: aeb964a11289fff3ee0c284f4af0425a8f0cc862de77609f80f246b8047a42de
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 698e4f197b91844a0eecc0ecf0357c63045c5f18f54ac7563f42ece3e98512d27b4d337781d50d7093e75f3fe3cc820229dc8ec4390527eff397966dcfa38451
|
7
|
+
data.tar.gz: 1117227e92345f6b68283aa540653a1825628436eb8d49377d6f7fbafe8d41e9699d9393b65803705e5327be1d8149030351066d18e979ffc1d5e19807d744ec
|
data/helpers/build
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
set -e
|
4
|
+
|
5
|
+
install_dir=$1
|
6
|
+
if [ -z "$install_dir" ]; then
|
7
|
+
echo "usage: $0 INSTALL_DIR"
|
8
|
+
exit 1
|
9
|
+
fi
|
10
|
+
|
11
|
+
helpers_dir="$(dirname "${BASH_SOURCE[0]}")"
|
12
|
+
cp -r \
|
13
|
+
"$helpers_dir/lib" \
|
14
|
+
"$helpers_dir/monkey_patches" \
|
15
|
+
"$helpers_dir/run.rb" \
|
16
|
+
"$install_dir"
|
17
|
+
|
18
|
+
cd "$install_dir"
|
@@ -0,0 +1,193 @@
|
|
1
|
+
require "functions/file_parser"
|
2
|
+
require "functions/force_updater"
|
3
|
+
require "functions/lockfile_updater"
|
4
|
+
require "functions/dependency_source"
|
5
|
+
require "functions/version_resolver"
|
6
|
+
require "functions/conflicting_dependency_resolver"
|
7
|
+
|
8
|
+
module Functions
|
9
|
+
def self.parsed_gemfile(lockfile_name:, gemfile_name:, dir:)
|
10
|
+
set_bundler_flags_and_credentials(dir: dir, credentials: [],
|
11
|
+
using_bundler2: false)
|
12
|
+
FileParser.new(lockfile_name: lockfile_name).
|
13
|
+
parsed_gemfile(gemfile_name: gemfile_name)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.parsed_gemspec(lockfile_name:, gemspec_name:, dir:)
|
17
|
+
set_bundler_flags_and_credentials(dir: dir, credentials: [],
|
18
|
+
using_bundler2: false)
|
19
|
+
FileParser.new(lockfile_name: lockfile_name).
|
20
|
+
parsed_gemspec(gemspec_name: gemspec_name)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.vendor_cache_dir(dir:)
|
24
|
+
set_bundler_flags_and_credentials(dir: dir, credentials: [],
|
25
|
+
using_bundler2: false)
|
26
|
+
Bundler.app_cache
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.update_lockfile(dir:, gemfile_name:, lockfile_name:, using_bundler2:,
|
30
|
+
credentials:, dependencies:)
|
31
|
+
set_bundler_flags_and_credentials(dir: dir, credentials: credentials,
|
32
|
+
using_bundler2: using_bundler2)
|
33
|
+
LockfileUpdater.new(
|
34
|
+
gemfile_name: gemfile_name,
|
35
|
+
lockfile_name: lockfile_name,
|
36
|
+
dependencies: dependencies
|
37
|
+
).run
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.force_update(dir:, dependency_name:, target_version:, gemfile_name:,
|
41
|
+
lockfile_name:, using_bundler2:, credentials:,
|
42
|
+
update_multiple_dependencies:)
|
43
|
+
set_bundler_flags_and_credentials(dir: dir, credentials: credentials,
|
44
|
+
using_bundler2: using_bundler2)
|
45
|
+
ForceUpdater.new(
|
46
|
+
dependency_name: dependency_name,
|
47
|
+
target_version: target_version,
|
48
|
+
gemfile_name: gemfile_name,
|
49
|
+
lockfile_name: lockfile_name,
|
50
|
+
update_multiple_dependencies: update_multiple_dependencies
|
51
|
+
).run
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.dependency_source_type(gemfile_name:, dependency_name:, dir:,
|
55
|
+
credentials:)
|
56
|
+
set_bundler_flags_and_credentials(dir: dir, credentials: credentials,
|
57
|
+
using_bundler2: false)
|
58
|
+
|
59
|
+
DependencySource.new(
|
60
|
+
gemfile_name: gemfile_name,
|
61
|
+
dependency_name: dependency_name
|
62
|
+
).type
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.depencency_source_latest_git_version(gemfile_name:, dependency_name:,
|
66
|
+
dir:, credentials:,
|
67
|
+
dependency_source_url:,
|
68
|
+
dependency_source_branch:)
|
69
|
+
set_bundler_flags_and_credentials(dir: dir, credentials: credentials,
|
70
|
+
using_bundler2: false)
|
71
|
+
DependencySource.new(
|
72
|
+
gemfile_name: gemfile_name,
|
73
|
+
dependency_name: dependency_name
|
74
|
+
).latest_git_version(
|
75
|
+
dependency_source_url: dependency_source_url,
|
76
|
+
dependency_source_branch: dependency_source_branch
|
77
|
+
)
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.private_registry_versions(gemfile_name:, dependency_name:, dir:,
|
81
|
+
credentials:)
|
82
|
+
set_bundler_flags_and_credentials(dir: dir, credentials: credentials,
|
83
|
+
using_bundler2: false)
|
84
|
+
|
85
|
+
DependencySource.new(
|
86
|
+
gemfile_name: gemfile_name,
|
87
|
+
dependency_name: dependency_name
|
88
|
+
).private_registry_versions
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.resolve_version(dependency_name:, dependency_requirements:,
|
92
|
+
gemfile_name:, lockfile_name:, using_bundler2:,
|
93
|
+
dir:, credentials:)
|
94
|
+
set_bundler_flags_and_credentials(dir: dir, credentials: credentials,
|
95
|
+
using_bundler2: using_bundler2)
|
96
|
+
VersionResolver.new(
|
97
|
+
dependency_name: dependency_name,
|
98
|
+
dependency_requirements: dependency_requirements,
|
99
|
+
gemfile_name: gemfile_name,
|
100
|
+
lockfile_name: lockfile_name
|
101
|
+
).version_details
|
102
|
+
end
|
103
|
+
|
104
|
+
def self.jfrog_source(dir:, gemfile_name:, credentials:, using_bundler2:)
|
105
|
+
# Set flags and credentials
|
106
|
+
set_bundler_flags_and_credentials(dir: dir, credentials: credentials,
|
107
|
+
using_bundler2: using_bundler2)
|
108
|
+
|
109
|
+
Bundler::Definition.build(gemfile_name, nil, {}).
|
110
|
+
send(:sources).
|
111
|
+
rubygems_remotes.
|
112
|
+
find { |uri| uri.host.include?("jfrog") }&.
|
113
|
+
host
|
114
|
+
end
|
115
|
+
|
116
|
+
def self.git_specs(dir:, gemfile_name:, credentials:, using_bundler2:)
|
117
|
+
set_bundler_flags_and_credentials(dir: dir, credentials: credentials,
|
118
|
+
using_bundler2: using_bundler2)
|
119
|
+
|
120
|
+
git_specs = Bundler::Definition.build(gemfile_name, nil, {}).dependencies.
|
121
|
+
select do |spec|
|
122
|
+
spec.source.is_a?(Bundler::Source::Git)
|
123
|
+
end
|
124
|
+
git_specs.map do |spec|
|
125
|
+
# Piggy-back off some private Bundler methods to configure the
|
126
|
+
# URI with auth details in the same way Bundler does.
|
127
|
+
git_proxy = spec.source.send(:git_proxy)
|
128
|
+
auth_uri = spec.source.uri.gsub("git://", "https://")
|
129
|
+
auth_uri = git_proxy.send(:configured_uri_for, auth_uri)
|
130
|
+
auth_uri += ".git" unless auth_uri.end_with?(".git")
|
131
|
+
auth_uri += "/info/refs?service=git-upload-pack"
|
132
|
+
{
|
133
|
+
uri: spec.source.uri,
|
134
|
+
auth_uri: auth_uri
|
135
|
+
}
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def self.set_bundler_flags_and_credentials(dir:, credentials:,
|
140
|
+
using_bundler2:)
|
141
|
+
dir = dir ? Pathname.new(dir) : dir
|
142
|
+
Bundler.instance_variable_set(:@root, dir)
|
143
|
+
|
144
|
+
# Remove installed gems from the default Rubygems index
|
145
|
+
Gem::Specification.all =
|
146
|
+
Gem::Specification.send(:default_stubs, "*.gemspec")
|
147
|
+
|
148
|
+
# Set auth details
|
149
|
+
relevant_credentials(credentials).each do |cred|
|
150
|
+
token = cred["token"] ||
|
151
|
+
"#{cred['username']}:#{cred['password']}"
|
152
|
+
|
153
|
+
Bundler.settings.set_command_option(
|
154
|
+
cred.fetch("host"),
|
155
|
+
token.gsub("@", "%40F").gsub("?", "%3F")
|
156
|
+
)
|
157
|
+
end
|
158
|
+
|
159
|
+
# Use HTTPS for GitHub if lockfile was generated by Bundler 2
|
160
|
+
if using_bundler2
|
161
|
+
Bundler.settings.set_command_option("forget_cli_options", "true")
|
162
|
+
Bundler.settings.set_command_option("github.https", "true")
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def self.relevant_credentials(credentials)
|
167
|
+
[
|
168
|
+
*git_source_credentials(credentials),
|
169
|
+
*private_registry_credentials(credentials)
|
170
|
+
].select { |cred| cred["password"] || cred["token"] }
|
171
|
+
end
|
172
|
+
|
173
|
+
def self.private_registry_credentials(credentials)
|
174
|
+
credentials.
|
175
|
+
select { |cred| cred["type"] == "rubygems_server" }
|
176
|
+
end
|
177
|
+
|
178
|
+
def self.git_source_credentials(credentials)
|
179
|
+
credentials.
|
180
|
+
select { |cred| cred["type"] == "git_source" }
|
181
|
+
end
|
182
|
+
|
183
|
+
def self.conflicting_dependencies(dir:, dependency_name:, target_version:,
|
184
|
+
lockfile_name:, using_bundler2:, credentials:)
|
185
|
+
set_bundler_flags_and_credentials(dir: dir, credentials: credentials,
|
186
|
+
using_bundler2: using_bundler2)
|
187
|
+
ConflictingDependencyResolver.new(
|
188
|
+
dependency_name: dependency_name,
|
189
|
+
target_version: target_version,
|
190
|
+
lockfile_name: lockfile_name
|
191
|
+
).conflicting_dependencies
|
192
|
+
end
|
193
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Functions
|
4
|
+
class ConflictingDependencyResolver
|
5
|
+
def initialize(dependency_name:, target_version:, lockfile_name:)
|
6
|
+
@dependency_name = dependency_name
|
7
|
+
@target_version = target_version
|
8
|
+
@lockfile_name = lockfile_name
|
9
|
+
end
|
10
|
+
|
11
|
+
# Finds any dependencies in the lockfile that have a subdependency on the
|
12
|
+
# given dependency that does not satisfly the target_version.
|
13
|
+
# @return [Array<Hash{String => String}]
|
14
|
+
# * explanation [String] a sentence explaining the conflict
|
15
|
+
# * name [String] the blocking dependencies name
|
16
|
+
# * version [String] the version of the blocking dependency
|
17
|
+
# * requirement [String] the requirement on the target_dependency
|
18
|
+
def conflicting_dependencies
|
19
|
+
Bundler.settings.set_command_option("only_update_to_newer_versions", true)
|
20
|
+
|
21
|
+
parent_specs.flat_map do |parent_spec|
|
22
|
+
top_level_specs_for(parent_spec).map do |top_level|
|
23
|
+
dependency = parent_spec.dependencies.find { |bd| bd.name == dependency_name }
|
24
|
+
{
|
25
|
+
"explanation" => explanation(parent_spec, dependency, top_level),
|
26
|
+
"name" => parent_spec.name,
|
27
|
+
"version" => parent_spec.version.to_s,
|
28
|
+
"requirement" => dependency.requirement.to_s
|
29
|
+
}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
attr_reader :dependency_name, :target_version, :lockfile_name
|
37
|
+
|
38
|
+
def parent_specs
|
39
|
+
version = Gem::Version.new(target_version)
|
40
|
+
parsed_lockfile.specs.filter do |spec|
|
41
|
+
spec.dependencies.any? do |dep|
|
42
|
+
dep.name == dependency_name &&
|
43
|
+
!dep.requirement.satisfied_by?(version)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def top_level_specs_for(parent_spec)
|
49
|
+
return [parent_spec] if top_level?(parent_spec)
|
50
|
+
|
51
|
+
parsed_lockfile.specs.filter do |spec|
|
52
|
+
spec.dependencies.any? do |dep|
|
53
|
+
dep.name == parent_spec.name && top_level?(spec)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def top_level?(spec)
|
59
|
+
parsed_lockfile.dependencies.key?(spec.name)
|
60
|
+
end
|
61
|
+
|
62
|
+
def explanation(spec, dependency, top_level)
|
63
|
+
if spec.name == top_level.name
|
64
|
+
"#{spec.name} (#{spec.version}) requires #{dependency_name} (#{dependency.requirement})"
|
65
|
+
else
|
66
|
+
"#{top_level.name} (#{top_level.version}) requires #{dependency_name} "\
|
67
|
+
"(#{dependency.requirement}) via #{spec.name} (#{spec.version})"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def parsed_lockfile
|
72
|
+
@parsed_lockfile ||= Bundler::LockfileParser.new(lockfile)
|
73
|
+
end
|
74
|
+
|
75
|
+
def lockfile
|
76
|
+
return @lockfile if defined?(@lockfile)
|
77
|
+
|
78
|
+
@lockfile =
|
79
|
+
begin
|
80
|
+
return unless lockfile_name && File.exist?(lockfile_name)
|
81
|
+
|
82
|
+
File.read(lockfile_name)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Functions
|
2
|
+
class DependencySource
|
3
|
+
attr_reader :gemfile_name, :dependency_name
|
4
|
+
|
5
|
+
RUBYGEMS = "rubygems"
|
6
|
+
PRIVATE_REGISTRY = "private"
|
7
|
+
GIT = "git"
|
8
|
+
OTHER = "other"
|
9
|
+
|
10
|
+
def initialize(gemfile_name:, dependency_name:)
|
11
|
+
@gemfile_name = gemfile_name
|
12
|
+
@dependency_name = dependency_name
|
13
|
+
end
|
14
|
+
|
15
|
+
def type
|
16
|
+
bundler_source = specified_source || default_source
|
17
|
+
type_of(bundler_source)
|
18
|
+
end
|
19
|
+
|
20
|
+
def latest_git_version(dependency_source_url:, dependency_source_branch:)
|
21
|
+
source = Bundler::Source::Git.new(
|
22
|
+
"uri" => dependency_source_url,
|
23
|
+
"branch" => dependency_source_branch,
|
24
|
+
"name" => dependency_name,
|
25
|
+
"submodules" => true
|
26
|
+
)
|
27
|
+
|
28
|
+
# Tell Bundler we're fine with fetching the source remotely
|
29
|
+
source.instance_variable_set(:@allow_remote, true)
|
30
|
+
|
31
|
+
spec = source.specs.first
|
32
|
+
{ version: spec.version, commit_sha: spec.source.revision }
|
33
|
+
end
|
34
|
+
|
35
|
+
def private_registry_versions
|
36
|
+
bundler_source = specified_source || default_source
|
37
|
+
|
38
|
+
bundler_source.
|
39
|
+
fetchers.flat_map do |fetcher|
|
40
|
+
fetcher.
|
41
|
+
specs_with_retry([dependency_name], bundler_source).
|
42
|
+
search_all(dependency_name)
|
43
|
+
end.
|
44
|
+
map(&:version)
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def type_of(bundler_source)
|
50
|
+
case bundler_source
|
51
|
+
when Bundler::Source::Rubygems
|
52
|
+
remote = bundler_source.remotes.first
|
53
|
+
if remote.nil? || remote.to_s == "https://rubygems.org/"
|
54
|
+
RUBYGEMS
|
55
|
+
else
|
56
|
+
PRIVATE_REGISTRY
|
57
|
+
end
|
58
|
+
when Bundler::Source::Git
|
59
|
+
GIT
|
60
|
+
else
|
61
|
+
OTHER
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def specified_source
|
66
|
+
return @specified_source if defined? @specified_source
|
67
|
+
|
68
|
+
@specified_source = definition.dependencies.
|
69
|
+
find { |dep| dep.name == dependency_name }&.source
|
70
|
+
end
|
71
|
+
|
72
|
+
def default_source
|
73
|
+
definition.send(:sources).default_source
|
74
|
+
end
|
75
|
+
|
76
|
+
def definition
|
77
|
+
@definition ||= Bundler::Definition.build(gemfile_name, nil, {})
|
78
|
+
end
|
79
|
+
|
80
|
+
def serialize_bundler_source(source)
|
81
|
+
{
|
82
|
+
type: source.class.to_s
|
83
|
+
}
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module Functions
|
2
|
+
class FileParser
|
3
|
+
def initialize(lockfile_name:)
|
4
|
+
@lockfile_name = lockfile_name
|
5
|
+
end
|
6
|
+
|
7
|
+
attr_reader :lockfile_name
|
8
|
+
|
9
|
+
def parsed_gemfile(gemfile_name:)
|
10
|
+
Bundler::Definition.build(gemfile_name, nil, {}).
|
11
|
+
dependencies.select(&:current_platform?).
|
12
|
+
reject { |dep| dep.source.is_a?(Bundler::Source::Gemspec) }.
|
13
|
+
map(&method(:serialize_bundler_dependency))
|
14
|
+
end
|
15
|
+
|
16
|
+
def parsed_gemspec(gemspec_name:)
|
17
|
+
Bundler.load_gemspec_uncached(gemspec_name).
|
18
|
+
dependencies.
|
19
|
+
map(&method(:serialize_bundler_dependency))
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def lockfile
|
25
|
+
return @lockfile if defined?(@lockfile)
|
26
|
+
|
27
|
+
@lockfile =
|
28
|
+
begin
|
29
|
+
return unless lockfile_name && File.exist?(lockfile_name)
|
30
|
+
|
31
|
+
File.read(lockfile_name)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def parsed_lockfile
|
36
|
+
return unless lockfile
|
37
|
+
|
38
|
+
@parsed_lockfile ||= Bundler::LockfileParser.new(lockfile)
|
39
|
+
end
|
40
|
+
|
41
|
+
def source_from_lockfile(dependency_name)
|
42
|
+
parsed_lockfile&.specs.find { |s| s.name == dependency_name }&.source
|
43
|
+
end
|
44
|
+
|
45
|
+
def source_for(dependency)
|
46
|
+
source = dependency.source
|
47
|
+
if lockfile && default_rubygems?(source)
|
48
|
+
# If there's a lockfile and the Gemfile doesn't have anything
|
49
|
+
# interesting to say about the source, check that.
|
50
|
+
source = source_from_lockfile(dependency.name)
|
51
|
+
end
|
52
|
+
raise "Bad source: #{source}" unless sources.include?(source.class)
|
53
|
+
|
54
|
+
return nil if default_rubygems?(source)
|
55
|
+
|
56
|
+
details = { type: source.class.name.split("::").last.downcase }
|
57
|
+
if source.is_a?(Bundler::Source::Git)
|
58
|
+
details.merge!(git_source_details(source))
|
59
|
+
end
|
60
|
+
if source.is_a?(Bundler::Source::Rubygems)
|
61
|
+
details[:url] = source.remotes.first.to_s
|
62
|
+
end
|
63
|
+
details
|
64
|
+
end
|
65
|
+
|
66
|
+
def git_source_details(source)
|
67
|
+
{
|
68
|
+
url: source.uri,
|
69
|
+
branch: source.branch || "master",
|
70
|
+
ref: source.ref
|
71
|
+
}
|
72
|
+
end
|
73
|
+
|
74
|
+
def default_rubygems?(source)
|
75
|
+
return true if source.nil?
|
76
|
+
return false unless source.is_a?(Bundler::Source::Rubygems)
|
77
|
+
|
78
|
+
source.remotes.any? { |r| r.to_s.include?("rubygems.org") }
|
79
|
+
end
|
80
|
+
|
81
|
+
def serialize_bundler_dependency(dependency)
|
82
|
+
{
|
83
|
+
name: dependency.name,
|
84
|
+
requirement: dependency.requirement,
|
85
|
+
groups: dependency.groups,
|
86
|
+
source: source_for(dependency),
|
87
|
+
type: dependency.type
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
# Can't be a constant because some of these don't exist in bundler
|
92
|
+
# 1.15, which used to cause issues on Heroku (causing exception on boot).
|
93
|
+
# TODO: Check if this will be an issue with multiple bundler versions
|
94
|
+
def sources
|
95
|
+
[
|
96
|
+
NilClass,
|
97
|
+
Bundler::Source::Rubygems,
|
98
|
+
Bundler::Source::Git,
|
99
|
+
Bundler::Source::Path,
|
100
|
+
Bundler::Source::Gemspec,
|
101
|
+
Bundler::Source::Metadata
|
102
|
+
]
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
module Functions
|
2
|
+
class ForceUpdater
|
3
|
+
class TransitiveDependencyError < StandardError; end
|
4
|
+
|
5
|
+
def initialize(dependency_name:, target_version:, gemfile_name:,
|
6
|
+
lockfile_name:, update_multiple_dependencies:)
|
7
|
+
@dependency_name = dependency_name
|
8
|
+
@target_version = target_version
|
9
|
+
@gemfile_name = gemfile_name
|
10
|
+
@lockfile_name = lockfile_name
|
11
|
+
@update_multiple_dependencies = update_multiple_dependencies
|
12
|
+
end
|
13
|
+
|
14
|
+
def run
|
15
|
+
# Only allow upgrades. Otherwise it's unlikely that this
|
16
|
+
# resolution will be found by the FileUpdater
|
17
|
+
Bundler.settings.set_command_option(
|
18
|
+
"only_update_to_newer_versions",
|
19
|
+
true
|
20
|
+
)
|
21
|
+
|
22
|
+
dependencies_to_unlock = []
|
23
|
+
|
24
|
+
begin
|
25
|
+
definition = build_definition(dependencies_to_unlock: dependencies_to_unlock)
|
26
|
+
definition.resolve_remotely!
|
27
|
+
specs = definition.resolve
|
28
|
+
updates = [{ name: dependency_name }] +
|
29
|
+
dependencies_to_unlock.map { |dep| { name: dep.name } }
|
30
|
+
specs = specs.map do |dep|
|
31
|
+
{
|
32
|
+
name: dep.name,
|
33
|
+
version: dep.version
|
34
|
+
}
|
35
|
+
end
|
36
|
+
[updates, specs]
|
37
|
+
rescue Bundler::VersionConflict => e
|
38
|
+
raise unless update_multiple_dependencies?
|
39
|
+
|
40
|
+
# TODO: Not sure this won't unlock way too many things...
|
41
|
+
new_dependencies_to_unlock =
|
42
|
+
new_dependencies_to_unlock_from(
|
43
|
+
error: e,
|
44
|
+
already_unlocked: dependencies_to_unlock
|
45
|
+
)
|
46
|
+
|
47
|
+
raise if new_dependencies_to_unlock.none?
|
48
|
+
|
49
|
+
dependencies_to_unlock += new_dependencies_to_unlock
|
50
|
+
retry
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
attr_reader :dependency_name, :target_version, :gemfile_name,
|
57
|
+
:lockfile_name, :credentials,
|
58
|
+
:update_multiple_dependencies
|
59
|
+
alias update_multiple_dependencies? update_multiple_dependencies
|
60
|
+
|
61
|
+
def new_dependencies_to_unlock_from(error:, already_unlocked:)
|
62
|
+
potentials_deps =
|
63
|
+
relevant_conflicts(error, already_unlocked).
|
64
|
+
flat_map(&:requirement_trees).
|
65
|
+
reject do |tree|
|
66
|
+
# If the final requirement wasn't specific, it can't be binding
|
67
|
+
next true if tree.last.requirement == Gem::Requirement.new(">= 0")
|
68
|
+
|
69
|
+
# If the conflict wasn't for the dependency we're updating then
|
70
|
+
# we don't have enough info to reject it
|
71
|
+
next false unless tree.last.name == dependency_name
|
72
|
+
|
73
|
+
# If the final requirement *was* for the dependency we're updating
|
74
|
+
# then we can ignore the tree if it permits the target version
|
75
|
+
tree.last.requirement.satisfied_by?(
|
76
|
+
Gem::Version.new(target_version)
|
77
|
+
)
|
78
|
+
end.map(&:first)
|
79
|
+
|
80
|
+
potentials_deps.
|
81
|
+
reject { |dep| already_unlocked.map(&:name).include?(dep.name) }.
|
82
|
+
reject { |dep| [dependency_name, "ruby\0"].include?(dep.name) }.
|
83
|
+
uniq
|
84
|
+
end
|
85
|
+
|
86
|
+
def relevant_conflicts(error, dependencies_being_unlocked)
|
87
|
+
names = [*dependencies_being_unlocked.map(&:name), dependency_name]
|
88
|
+
|
89
|
+
# For a conflict to be relevant to the updates we're making it must be
|
90
|
+
# 1) caused by a new requirement introduced by our unlocking, or
|
91
|
+
# 2) caused by an old requirement that prohibits the update.
|
92
|
+
# Hence, we look at the beginning and end of the requirement trees
|
93
|
+
error.cause.conflicts.values.
|
94
|
+
select do |conflict|
|
95
|
+
conflict.requirement_trees.any? do |t|
|
96
|
+
names.include?(t.last.name) || names.include?(t.first.name)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def build_definition(dependencies_to_unlock:)
|
102
|
+
gems_to_unlock = dependencies_to_unlock.map(&:name) + [dependency_name]
|
103
|
+
definition = Bundler::Definition.build(
|
104
|
+
gemfile_name,
|
105
|
+
lockfile_name,
|
106
|
+
gems: gems_to_unlock + subdependencies,
|
107
|
+
lock_shared_dependencies: true
|
108
|
+
)
|
109
|
+
|
110
|
+
# Remove the Gemfile / gemspec requirements on the gems we're
|
111
|
+
# unlocking (i.e., completely unlock them)
|
112
|
+
gems_to_unlock.each do |gem_name|
|
113
|
+
unlock_gem(definition: definition, gem_name: gem_name)
|
114
|
+
end
|
115
|
+
|
116
|
+
dep = definition.dependencies.
|
117
|
+
find { |d| d.name == dependency_name }
|
118
|
+
|
119
|
+
# If the dependency is not found in the Gemfile it means this is a
|
120
|
+
# transitive dependency that we can't force update.
|
121
|
+
raise TransitiveDependencyError unless dep
|
122
|
+
|
123
|
+
# Set the requirement for the gem we're forcing an update of
|
124
|
+
new_req = Gem::Requirement.create("= #{target_version}")
|
125
|
+
dep.instance_variable_set(:@requirement, new_req)
|
126
|
+
dep.source = nil if dep.source.is_a?(Bundler::Source::Git)
|
127
|
+
|
128
|
+
definition
|
129
|
+
end
|
130
|
+
|
131
|
+
def lockfile
|
132
|
+
return @lockfile if defined?(@lockfile)
|
133
|
+
|
134
|
+
@lockfile =
|
135
|
+
begin
|
136
|
+
return unless lockfile_name && File.exist?(lockfile_name)
|
137
|
+
|
138
|
+
File.read(lockfile_name)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def subdependencies
|
143
|
+
# If there's no lockfile we don't need to worry about
|
144
|
+
# subdependencies
|
145
|
+
return [] unless lockfile
|
146
|
+
|
147
|
+
all_deps = Bundler::LockfileParser.new(lockfile).
|
148
|
+
specs.map(&:name).map(&:to_s)
|
149
|
+
top_level = Bundler::Definition.
|
150
|
+
build(gemfile_name, lockfile_name, {}).
|
151
|
+
dependencies.map(&:name).map(&:to_s)
|
152
|
+
|
153
|
+
all_deps - top_level
|
154
|
+
end
|
155
|
+
|
156
|
+
def unlock_gem(definition:, gem_name:)
|
157
|
+
dep = definition.dependencies.find { |d| d.name == gem_name }
|
158
|
+
version = definition.locked_gems.specs.
|
159
|
+
find { |d| d.name == gem_name }.version
|
160
|
+
|
161
|
+
dep&.instance_variable_set(
|
162
|
+
:@requirement,
|
163
|
+
Gem::Requirement.create(">= #{version}")
|
164
|
+
)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
@@ -0,0 +1,224 @@
|
|
1
|
+
module Functions
|
2
|
+
class LockfileUpdater
|
3
|
+
RETRYABLE_ERRORS = [Bundler::HTTPError].freeze
|
4
|
+
GEM_NOT_FOUND_ERROR_REGEX =
|
5
|
+
/
|
6
|
+
locked\sto\s(?<name>[^\s]+)\s\(|
|
7
|
+
not\sfind\s(?<name>[^\s]+)-\d|
|
8
|
+
has\s(?<name>[^\s]+)\slocked\sat
|
9
|
+
/x.freeze
|
10
|
+
|
11
|
+
def initialize(gemfile_name:, lockfile_name:, dependencies:)
|
12
|
+
@gemfile_name = gemfile_name
|
13
|
+
@lockfile_name = lockfile_name
|
14
|
+
@dependencies = dependencies
|
15
|
+
end
|
16
|
+
|
17
|
+
def run
|
18
|
+
generate_lockfile
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
attr_reader :gemfile_name, :lockfile_name, :dependencies
|
24
|
+
|
25
|
+
def generate_lockfile
|
26
|
+
dependencies_to_unlock = dependencies.map { |d| d.fetch("name") }
|
27
|
+
|
28
|
+
begin
|
29
|
+
definition = build_definition(dependencies_to_unlock)
|
30
|
+
|
31
|
+
old_reqs = lock_deps_being_updated_to_exact_versions(definition)
|
32
|
+
|
33
|
+
definition.resolve_remotely!
|
34
|
+
|
35
|
+
old_reqs.each do |dep_name, old_req|
|
36
|
+
d_dep = definition.dependencies.find { |d| d.name == dep_name }
|
37
|
+
if old_req == :none then definition.dependencies.delete(d_dep)
|
38
|
+
else
|
39
|
+
d_dep.instance_variable_set(:@requirement, old_req)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
cache_vendored_gems(definition) if Bundler.app_cache.exist?
|
44
|
+
|
45
|
+
definition.to_lock
|
46
|
+
rescue Bundler::GemNotFound => e
|
47
|
+
unlock_yanked_gem(dependencies_to_unlock, e) && retry
|
48
|
+
rescue Bundler::VersionConflict => e
|
49
|
+
unlock_blocking_subdeps(dependencies_to_unlock, e) && retry
|
50
|
+
rescue *RETRYABLE_ERRORS
|
51
|
+
raise if @retrying
|
52
|
+
|
53
|
+
@retrying = true
|
54
|
+
sleep(rand(1.0..5.0))
|
55
|
+
retry
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def cache_vendored_gems(definition)
|
60
|
+
# Dependencies that have been unlocked for the update (including
|
61
|
+
# sub-dependencies)
|
62
|
+
unlocked_gems = definition.instance_variable_get(:@unlock).
|
63
|
+
fetch(:gems).reject { |gem| __keep_on_prune?(gem) }
|
64
|
+
bundler_opts = {
|
65
|
+
cache_all: true,
|
66
|
+
cache_all_platforms: true,
|
67
|
+
no_prune: true
|
68
|
+
}
|
69
|
+
|
70
|
+
Bundler.settings.temporary(**bundler_opts) do
|
71
|
+
# Fetch and cache gems on all platforms without pruning
|
72
|
+
Bundler::Runtime.new(nil, definition).cache
|
73
|
+
|
74
|
+
# Only prune unlocked gems (the original implementation is in
|
75
|
+
# Bundler::Runtime)
|
76
|
+
cache_path = Bundler.app_cache
|
77
|
+
resolve = definition.resolve
|
78
|
+
prune_gem_cache(resolve, cache_path, unlocked_gems)
|
79
|
+
prune_git_and_path_cache(resolve, cache_path)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# This is not officially supported and may be removed without notice.
|
84
|
+
def __keep_on_prune?(spec_name)
|
85
|
+
unless (specs = Bundler.settings[:persistent_gems_after_clean])
|
86
|
+
return false
|
87
|
+
end
|
88
|
+
|
89
|
+
specs.include?(spec_name)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Copied from Bundler::Runtime: Modified to only prune gems that have
|
93
|
+
# been unlocked
|
94
|
+
def prune_gem_cache(resolve, cache_path, unlocked_gems)
|
95
|
+
cached_gems = Dir["#{cache_path}/*.gem"]
|
96
|
+
|
97
|
+
outdated_gems = cached_gems.reject do |path|
|
98
|
+
spec = Bundler.rubygems.spec_from_gem path
|
99
|
+
|
100
|
+
!unlocked_gems.include?(spec.name) || resolve.any? do |s|
|
101
|
+
s.name == spec.name && s.version == spec.version &&
|
102
|
+
!s.source.is_a?(Bundler::Source::Git)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
return unless outdated_gems.any?
|
107
|
+
|
108
|
+
outdated_gems.each do |path|
|
109
|
+
File.delete(path)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Copied from Bundler::Runtime
|
114
|
+
def prune_git_and_path_cache(resolve, cache_path)
|
115
|
+
cached_git_and_path = Dir["#{cache_path}/*/.bundlecache"]
|
116
|
+
|
117
|
+
outdated_git_and_path = cached_git_and_path.reject do |path|
|
118
|
+
name = File.basename(File.dirname(path))
|
119
|
+
|
120
|
+
resolve.any? do |s|
|
121
|
+
s.source.respond_to?(:app_cache_dirname) &&
|
122
|
+
s.source.app_cache_dirname == name
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
return unless outdated_git_and_path.any?
|
127
|
+
|
128
|
+
outdated_git_and_path.each do |path|
|
129
|
+
path = File.dirname(path)
|
130
|
+
FileUtils.rm_rf(path)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def unlock_yanked_gem(dependencies_to_unlock, error)
|
135
|
+
raise unless error.message.match?(GEM_NOT_FOUND_ERROR_REGEX)
|
136
|
+
|
137
|
+
gem_name = error.message.match(GEM_NOT_FOUND_ERROR_REGEX).
|
138
|
+
named_captures["name"]
|
139
|
+
raise if dependencies_to_unlock.include?(gem_name)
|
140
|
+
|
141
|
+
dependencies_to_unlock << gem_name
|
142
|
+
end
|
143
|
+
|
144
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
145
|
+
def unlock_blocking_subdeps(dependencies_to_unlock, error)
|
146
|
+
all_deps = Bundler::LockfileParser.new(lockfile).
|
147
|
+
specs.map(&:name).map(&:to_s)
|
148
|
+
top_level = build_definition([]).dependencies.
|
149
|
+
map(&:name).map(&:to_s)
|
150
|
+
allowed_new_unlocks = all_deps - top_level - dependencies_to_unlock
|
151
|
+
|
152
|
+
raise if allowed_new_unlocks.none?
|
153
|
+
|
154
|
+
# Unlock any sub-dependencies that Bundler reports caused the
|
155
|
+
# conflict
|
156
|
+
potentials_deps =
|
157
|
+
error.cause.conflicts.values.
|
158
|
+
flat_map(&:requirement_trees).
|
159
|
+
map do |tree|
|
160
|
+
tree.find { |req| allowed_new_unlocks.include?(req.name) }
|
161
|
+
end.compact.map(&:name)
|
162
|
+
|
163
|
+
# If there are specific dependencies we can unlock, unlock them
|
164
|
+
if potentials_deps.any?
|
165
|
+
return dependencies_to_unlock.append(*potentials_deps)
|
166
|
+
end
|
167
|
+
|
168
|
+
# Fall back to unlocking *all* sub-dependencies. This is required
|
169
|
+
# because Bundler's VersionConflict objects don't include enough
|
170
|
+
# information to chart the full path through all conflicts unwound
|
171
|
+
dependencies_to_unlock.append(*allowed_new_unlocks)
|
172
|
+
end
|
173
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
174
|
+
|
175
|
+
def build_definition(dependencies_to_unlock)
|
176
|
+
defn = Bundler::Definition.build(
|
177
|
+
gemfile_name,
|
178
|
+
lockfile_name,
|
179
|
+
gems: dependencies_to_unlock
|
180
|
+
)
|
181
|
+
|
182
|
+
# Bundler unlocks the sub-dependencies of gems it is passed even
|
183
|
+
# if those sub-deps are top-level dependencies. We only want true
|
184
|
+
# subdeps unlocked, like they were in the UpdateChecker, so we
|
185
|
+
# mutate the unlocked gems array.
|
186
|
+
unlocked = defn.instance_variable_get(:@unlock).fetch(:gems)
|
187
|
+
must_not_unlock = defn.dependencies.map(&:name).map(&:to_s) -
|
188
|
+
dependencies_to_unlock
|
189
|
+
unlocked.reject! { |n| must_not_unlock.include?(n) }
|
190
|
+
|
191
|
+
defn
|
192
|
+
end
|
193
|
+
|
194
|
+
def lock_deps_being_updated_to_exact_versions(definition)
|
195
|
+
dependencies.each_with_object({}) do |dep, old_reqs|
|
196
|
+
defn_dep = definition.dependencies.find do |d|
|
197
|
+
d.name == dep.fetch("name")
|
198
|
+
end
|
199
|
+
|
200
|
+
if defn_dep.nil?
|
201
|
+
definition.dependencies <<
|
202
|
+
Bundler::Dependency.new(dep.fetch("name"), dep.fetch("version"))
|
203
|
+
old_reqs[dep.fetch("name")] = :none
|
204
|
+
elsif git_dependency?(dep) &&
|
205
|
+
defn_dep.source.is_a?(Bundler::Source::Git)
|
206
|
+
defn_dep.source.unlock!
|
207
|
+
elsif Gem::Version.correct?(dep.fetch("version"))
|
208
|
+
new_req = Gem::Requirement.create("= #{dep.fetch("version")}")
|
209
|
+
old_reqs[dep.fetch("name")] = defn_dep.requirement
|
210
|
+
defn_dep.instance_variable_set(:@requirement, new_req)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def git_dependency?(dep)
|
216
|
+
sources = dep.fetch("requirements").map { |r| r.fetch("source") }
|
217
|
+
sources.all? { |s| s&.fetch("type", nil) == "git" }
|
218
|
+
end
|
219
|
+
|
220
|
+
def lockfile
|
221
|
+
@lockfile ||= File.read(lockfile_name)
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
module Functions
|
2
|
+
class VersionResolver
|
3
|
+
GEM_NOT_FOUND_ERROR_REGEX = /locked to (?<name>[^\s]+) \(/.freeze
|
4
|
+
|
5
|
+
attr_reader :dependency_name, :dependency_requirements,
|
6
|
+
:gemfile_name, :lockfile_name
|
7
|
+
|
8
|
+
def initialize(dependency_name:, dependency_requirements:,
|
9
|
+
gemfile_name:, lockfile_name:)
|
10
|
+
@dependency_name = dependency_name
|
11
|
+
@dependency_requirements = dependency_requirements
|
12
|
+
@gemfile_name = gemfile_name
|
13
|
+
@lockfile_name = lockfile_name
|
14
|
+
end
|
15
|
+
|
16
|
+
def version_details
|
17
|
+
dep = dependency_from_definition
|
18
|
+
|
19
|
+
# If the dependency wasn't found in the definition, but *is*
|
20
|
+
# included in a gemspec, it's because the Gemfile didn't import
|
21
|
+
# the gemspec. This is unusual, but the correct behaviour if/when
|
22
|
+
# it happens is to behave as if the repo was gemspec-only.
|
23
|
+
if dep.nil? && dependency_requirements.any?
|
24
|
+
return "latest"
|
25
|
+
end
|
26
|
+
|
27
|
+
# Otherwise, if the dependency wasn't found it's because it is a
|
28
|
+
# subdependency that was removed when attempting to update it.
|
29
|
+
return nil if dep.nil?
|
30
|
+
|
31
|
+
# If the dependency is Bundler itself then we can't trust the
|
32
|
+
# version that has been returned (it's the version Dependabot is
|
33
|
+
# running on, rather than the true latest resolvable version).
|
34
|
+
return nil if dep.name == "bundler"
|
35
|
+
|
36
|
+
details = {
|
37
|
+
version: dep.version,
|
38
|
+
ruby_version: ruby_version,
|
39
|
+
fetcher: fetcher_class(dep)
|
40
|
+
}
|
41
|
+
if dep.source.instance_of?(::Bundler::Source::Git)
|
42
|
+
details[:commit_sha] = dep.source.revision
|
43
|
+
end
|
44
|
+
details
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
50
|
+
def dependency_from_definition(unlock_subdependencies: true)
|
51
|
+
dependencies_to_unlock = [dependency_name]
|
52
|
+
dependencies_to_unlock += subdependencies if unlock_subdependencies
|
53
|
+
begin
|
54
|
+
definition = build_definition(dependencies_to_unlock)
|
55
|
+
definition.resolve_remotely!
|
56
|
+
rescue ::Bundler::GemNotFound => e
|
57
|
+
unlock_yanked_gem(dependencies_to_unlock, e) && retry
|
58
|
+
rescue ::Bundler::HTTPError => e
|
59
|
+
# Retry network errors
|
60
|
+
# Note: in_a_native_bundler_context will also retry `Bundler::HTTPError` errors
|
61
|
+
# up to three times meaning we'll end up retrying this error up to six times
|
62
|
+
# TODO: Could we get rid of this retry logic and only rely on
|
63
|
+
# SharedBundlerHelpers.in_a_native_bundler_context
|
64
|
+
attempt ||= 1
|
65
|
+
attempt += 1
|
66
|
+
raise if attempt > 3 || !e.message.include?("Network error")
|
67
|
+
|
68
|
+
retry
|
69
|
+
end
|
70
|
+
|
71
|
+
dep = definition.resolve.find { |d| d.name == dependency_name }
|
72
|
+
return dep if dep
|
73
|
+
return if dependency_requirements.any? || !unlock_subdependencies
|
74
|
+
|
75
|
+
# If no definition was found and we're updating a sub-dependency,
|
76
|
+
# try again but without unlocking any other sub-dependencies
|
77
|
+
dependency_from_definition(unlock_subdependencies: false)
|
78
|
+
end
|
79
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
80
|
+
|
81
|
+
def subdependencies
|
82
|
+
# If there's no lockfile we don't need to worry about
|
83
|
+
# subdependencies
|
84
|
+
return [] unless lockfile
|
85
|
+
|
86
|
+
all_deps = ::Bundler::LockfileParser.new(lockfile).
|
87
|
+
specs.map(&:name).map(&:to_s).uniq
|
88
|
+
top_level = build_definition([]).dependencies.
|
89
|
+
map(&:name).map(&:to_s)
|
90
|
+
|
91
|
+
all_deps - top_level
|
92
|
+
end
|
93
|
+
|
94
|
+
def build_definition(dependencies_to_unlock)
|
95
|
+
# Note: we lock shared dependencies to avoid any top-level
|
96
|
+
# dependencies getting unlocked (which would happen if they were
|
97
|
+
# also subdependencies of the dependency being unlocked)
|
98
|
+
::Bundler::Definition.build(
|
99
|
+
gemfile_name,
|
100
|
+
lockfile_name,
|
101
|
+
gems: dependencies_to_unlock,
|
102
|
+
lock_shared_dependencies: true
|
103
|
+
)
|
104
|
+
end
|
105
|
+
|
106
|
+
def unlock_yanked_gem(dependencies_to_unlock, error)
|
107
|
+
raise unless error.message.match?(GEM_NOT_FOUND_ERROR_REGEX)
|
108
|
+
|
109
|
+
gem_name = error.message.match(GEM_NOT_FOUND_ERROR_REGEX).
|
110
|
+
named_captures["name"]
|
111
|
+
raise if dependencies_to_unlock.include?(gem_name)
|
112
|
+
|
113
|
+
dependencies_to_unlock << gem_name
|
114
|
+
end
|
115
|
+
|
116
|
+
def lockfile
|
117
|
+
return @lockfile if defined?(@lockfile)
|
118
|
+
|
119
|
+
@lockfile =
|
120
|
+
begin
|
121
|
+
return unless lockfile_name
|
122
|
+
return unless File.exist?(lockfile_name)
|
123
|
+
|
124
|
+
File.read(lockfile_name)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def fetcher_class(dep)
|
129
|
+
return unless dep.source.is_a?(::Bundler::Source::Rubygems)
|
130
|
+
|
131
|
+
dep.source.fetchers.first.fetchers.first.class.to_s
|
132
|
+
end
|
133
|
+
|
134
|
+
def ruby_version
|
135
|
+
return nil unless gemfile_name
|
136
|
+
|
137
|
+
@ruby_version ||= build_definition([]).ruby_version&.gem_version
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/definition"
|
4
|
+
|
5
|
+
# Ignore the Bundler version specified in the Gemfile (since the only Bundler
|
6
|
+
# version available to us is the one we're using).
|
7
|
+
module BundlerDefinitionBundlerVersionPatch
|
8
|
+
def expanded_dependencies
|
9
|
+
@expanded_dependencies ||=
|
10
|
+
expand_dependencies(dependencies + metadata_dependencies, @remote).
|
11
|
+
reject { |d| d.name == "bundler" }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
Bundler::Definition.prepend(BundlerDefinitionBundlerVersionPatch)
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/definition"
|
4
|
+
|
5
|
+
module BundlerDefinitionRubyVersionPatch
|
6
|
+
def index
|
7
|
+
@index ||= super.tap do
|
8
|
+
if ruby_version
|
9
|
+
requested_version = ruby_version.to_gem_version_with_patchlevel
|
10
|
+
sources.metadata_source.specs <<
|
11
|
+
Gem::Specification.new("ruby\0", requested_version)
|
12
|
+
end
|
13
|
+
|
14
|
+
sources.metadata_source.specs <<
|
15
|
+
Gem::Specification.new("ruby\0", "2.5.3p105")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
Bundler::Definition.prepend(BundlerDefinitionRubyVersionPatch)
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/source"
|
4
|
+
|
5
|
+
module Bundler
|
6
|
+
class Source
|
7
|
+
class Git
|
8
|
+
class GitProxy
|
9
|
+
private
|
10
|
+
|
11
|
+
# Bundler allows ssh authentication when talking to GitHub but there's
|
12
|
+
# no way for Dependabot to do so (it doesn't have any ssh keys).
|
13
|
+
# Instead, we convert all `git@github.com:` URLs to use HTTPS.
|
14
|
+
def configured_uri_for(uri)
|
15
|
+
uri = uri.gsub(%r{git@(.*?):/?}, 'https://\1/')
|
16
|
+
if uri.match?(/https?:/)
|
17
|
+
remote = URI(uri)
|
18
|
+
config_auth =
|
19
|
+
Bundler.settings[remote.to_s] || Bundler.settings[remote.host]
|
20
|
+
remote.userinfo ||= config_auth
|
21
|
+
remote.to_s
|
22
|
+
else
|
23
|
+
uri
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
module Bundler
|
32
|
+
class Source
|
33
|
+
class Git < Path
|
34
|
+
private
|
35
|
+
|
36
|
+
def serialize_gemspecs_in(destination)
|
37
|
+
original_load_paths = $LOAD_PATH.dup
|
38
|
+
reduced_load_paths = original_load_paths.
|
39
|
+
reject { |p| p.include?("/gems/") }
|
40
|
+
|
41
|
+
$LOAD_PATH.shift until $LOAD_PATH.empty?
|
42
|
+
reduced_load_paths.each { |p| $LOAD_PATH << p }
|
43
|
+
|
44
|
+
if destination.relative?
|
45
|
+
destination = destination.expand_path(Bundler.root)
|
46
|
+
end
|
47
|
+
Dir["#{destination}/#{@glob}"].each do |spec_path|
|
48
|
+
# Evaluate gemspecs and cache the result. Gemspecs
|
49
|
+
# in git might require git or other dependencies.
|
50
|
+
# The gemspecs we cache should already be evaluated.
|
51
|
+
spec = Bundler.load_gemspec(spec_path)
|
52
|
+
next unless spec
|
53
|
+
|
54
|
+
Bundler.rubygems.set_installed_by_version(spec)
|
55
|
+
Bundler.rubygems.validate(spec)
|
56
|
+
File.open(spec_path, "wb") { |file| file.write(spec.to_ruby) }
|
57
|
+
end
|
58
|
+
$LOAD_PATH.shift until $LOAD_PATH.empty?
|
59
|
+
original_load_paths.each { |p| $LOAD_PATH << p }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/helpers/run.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require "bundler"
|
2
|
+
require "json"
|
3
|
+
|
4
|
+
$LOAD_PATH.unshift(File.expand_path("./lib", __dir__))
|
5
|
+
$LOAD_PATH.unshift(File.expand_path("./monkey_patches", __dir__))
|
6
|
+
|
7
|
+
# Bundler monkey patches
|
8
|
+
require "definition_ruby_version_patch"
|
9
|
+
require "definition_bundler_version_patch"
|
10
|
+
require "git_source_patch"
|
11
|
+
|
12
|
+
require "functions"
|
13
|
+
|
14
|
+
def output(obj)
|
15
|
+
print JSON.dump(obj)
|
16
|
+
end
|
17
|
+
|
18
|
+
begin
|
19
|
+
request = JSON.parse($stdin.read)
|
20
|
+
|
21
|
+
function = request["function"]
|
22
|
+
args = request["args"].transform_keys(&:to_sym)
|
23
|
+
|
24
|
+
output({ result: Functions.send(function, **args) })
|
25
|
+
rescue => error
|
26
|
+
output(
|
27
|
+
{ error: error.message, error_class: error.class, trace: error.backtrace }
|
28
|
+
)
|
29
|
+
exit(1)
|
30
|
+
end
|
@@ -7,8 +7,11 @@ module Dependabot
|
|
7
7
|
module Bundler
|
8
8
|
class FileUpdater
|
9
9
|
class RubyRequirementSetter
|
10
|
-
|
11
|
-
|
10
|
+
class RubyVersionNotFound < StandardError; end
|
11
|
+
|
12
|
+
RUBY_VERSIONS = %w(
|
13
|
+
1.8.7 1.9.3 2.0.0 2.1.10 2.2.10 2.3.8 2.4.10 2.5.8 2.6.6 2.7.2 3.0.0
|
14
|
+
).freeze
|
12
15
|
|
13
16
|
attr_reader :gemspec
|
14
17
|
|
@@ -53,7 +56,7 @@ module Dependabot
|
|
53
56
|
map { |v| Gem::Version.new(v) }.sort.
|
54
57
|
find { |v| requirement.satisfied_by?(v) }
|
55
58
|
|
56
|
-
raise
|
59
|
+
raise RubyVersionNotFound unless ruby_version
|
57
60
|
|
58
61
|
ruby_version
|
59
62
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dependabot-bundler
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.130.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dependabot
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-01-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: dependabot-common
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - '='
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 0.
|
19
|
+
version: 0.130.0
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - '='
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 0.
|
26
|
+
version: 0.130.0
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: byebug
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -100,28 +100,28 @@ dependencies:
|
|
100
100
|
requirements:
|
101
101
|
- - "~>"
|
102
102
|
- !ruby/object:Gem::Version
|
103
|
-
version: 1.
|
103
|
+
version: 1.8.0
|
104
104
|
type: :development
|
105
105
|
prerelease: false
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
107
107
|
requirements:
|
108
108
|
- - "~>"
|
109
109
|
- !ruby/object:Gem::Version
|
110
|
-
version: 1.
|
110
|
+
version: 1.8.0
|
111
111
|
- !ruby/object:Gem::Dependency
|
112
112
|
name: simplecov
|
113
113
|
requirement: !ruby/object:Gem::Requirement
|
114
114
|
requirements:
|
115
115
|
- - "~>"
|
116
116
|
- !ruby/object:Gem::Version
|
117
|
-
version: 0.
|
117
|
+
version: 0.21.0
|
118
118
|
type: :development
|
119
119
|
prerelease: false
|
120
120
|
version_requirements: !ruby/object:Gem::Requirement
|
121
121
|
requirements:
|
122
122
|
- - "~>"
|
123
123
|
- !ruby/object:Gem::Version
|
124
|
-
version: 0.
|
124
|
+
version: 0.21.0
|
125
125
|
- !ruby/object:Gem::Dependency
|
126
126
|
name: simplecov-console
|
127
127
|
requirement: !ruby/object:Gem::Requirement
|
@@ -171,6 +171,18 @@ executables: []
|
|
171
171
|
extensions: []
|
172
172
|
extra_rdoc_files: []
|
173
173
|
files:
|
174
|
+
- helpers/build
|
175
|
+
- helpers/lib/functions.rb
|
176
|
+
- helpers/lib/functions/conflicting_dependency_resolver.rb
|
177
|
+
- helpers/lib/functions/dependency_source.rb
|
178
|
+
- helpers/lib/functions/file_parser.rb
|
179
|
+
- helpers/lib/functions/force_updater.rb
|
180
|
+
- helpers/lib/functions/lockfile_updater.rb
|
181
|
+
- helpers/lib/functions/version_resolver.rb
|
182
|
+
- helpers/monkey_patches/definition_bundler_version_patch.rb
|
183
|
+
- helpers/monkey_patches/definition_ruby_version_patch.rb
|
184
|
+
- helpers/monkey_patches/git_source_patch.rb
|
185
|
+
- helpers/run.rb
|
174
186
|
- lib/dependabot/bundler.rb
|
175
187
|
- lib/dependabot/bundler/file_fetcher.rb
|
176
188
|
- lib/dependabot/bundler/file_fetcher/child_gemfile_finder.rb
|