dependabot-bundler 0.95.6 → 0.95.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/lib/dependabot/bundler/file_fetcher/child_gemfile_finder.rb +68 -0
  3. data/lib/dependabot/bundler/file_fetcher/gemspec_finder.rb +96 -0
  4. data/lib/dependabot/bundler/file_fetcher/path_gemspec_finder.rb +112 -0
  5. data/lib/dependabot/bundler/file_fetcher/require_relative_finder.rb +65 -0
  6. data/lib/dependabot/bundler/file_fetcher.rb +216 -0
  7. data/lib/dependabot/bundler/file_parser/file_preparer.rb +84 -0
  8. data/lib/dependabot/bundler/file_parser/gemfile_checker.rb +46 -0
  9. data/lib/dependabot/bundler/file_parser.rb +297 -0
  10. data/lib/dependabot/bundler/file_updater/gemfile_updater.rb +114 -0
  11. data/lib/dependabot/bundler/file_updater/gemspec_dependency_name_finder.rb +50 -0
  12. data/lib/dependabot/bundler/file_updater/gemspec_sanitizer.rb +298 -0
  13. data/lib/dependabot/bundler/file_updater/gemspec_updater.rb +62 -0
  14. data/lib/dependabot/bundler/file_updater/git_pin_replacer.rb +78 -0
  15. data/lib/dependabot/bundler/file_updater/git_source_remover.rb +100 -0
  16. data/lib/dependabot/bundler/file_updater/lockfile_updater.rb +387 -0
  17. data/lib/dependabot/bundler/file_updater/requirement_replacer.rb +221 -0
  18. data/lib/dependabot/bundler/file_updater.rb +125 -0
  19. data/lib/dependabot/bundler/metadata_finder.rb +204 -0
  20. data/lib/dependabot/bundler/requirement.rb +29 -0
  21. data/lib/dependabot/bundler/update_checker/file_preparer.rb +279 -0
  22. data/lib/dependabot/bundler/update_checker/force_updater.rb +259 -0
  23. data/lib/dependabot/bundler/update_checker/latest_version_finder.rb +165 -0
  24. data/lib/dependabot/bundler/update_checker/requirements_updater.rb +281 -0
  25. data/lib/dependabot/bundler/update_checker/ruby_requirement_setter.rb +113 -0
  26. data/lib/dependabot/bundler/update_checker/shared_bundler_helpers.rb +244 -0
  27. data/lib/dependabot/bundler/update_checker/version_resolver.rb +272 -0
  28. data/lib/dependabot/bundler/update_checker.rb +334 -0
  29. data/lib/dependabot/bundler/version.rb +13 -0
  30. data/lib/dependabot/bundler.rb +27 -0
  31. data/lib/dependabot/monkey_patches/bundler/definition_bundler_version_patch.rb +15 -0
  32. data/lib/dependabot/monkey_patches/bundler/definition_ruby_version_patch.rb +14 -0
  33. data/lib/dependabot/monkey_patches/bundler/git_source_patch.rb +27 -0
  34. metadata +37 -5
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "parser/current"
4
+ require "dependabot/bundler/update_checker"
5
+
6
+ module Dependabot
7
+ module Bundler
8
+ class UpdateChecker
9
+ class RubyRequirementSetter
10
+ RUBY_VERSIONS =
11
+ %w(1.8.7 1.9.3 2.0.0 2.1.10 2.2.10 2.3.8 2.4.5 2.5.3 2.6.0).freeze
12
+
13
+ attr_reader :gemspec
14
+
15
+ def initialize(gemspec:)
16
+ @gemspec = gemspec
17
+ end
18
+
19
+ def rewrite(content)
20
+ return content unless gemspec_declares_ruby_requirement?
21
+
22
+ buffer = Parser::Source::Buffer.new("(gemfile_content)")
23
+ buffer.source = content
24
+ ast = Parser::CurrentRuby.new.parse(buffer)
25
+
26
+ if declares_ruby_version?(ast)
27
+ GemfileRewriter.new(
28
+ ruby_version: ruby_version
29
+ ).rewrite(buffer, ast)
30
+ else
31
+ "ruby '#{ruby_version}'\n" + content
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def gemspec_declares_ruby_requirement?
38
+ !ruby_requirement.nil?
39
+ end
40
+
41
+ def declares_ruby_version?(node)
42
+ return false unless node.is_a?(Parser::AST::Node)
43
+ return true if node.type == :send && node.children[1] == :ruby
44
+
45
+ node.children.any? { |cn| declares_ruby_version?(cn) }
46
+ end
47
+
48
+ def ruby_version
49
+ requirement = Gem::Requirement.new(ruby_requirement)
50
+
51
+ ruby_version =
52
+ RUBY_VERSIONS.
53
+ map { |v| Gem::Version.new(v) }.sort.
54
+ find { |v| requirement.satisfied_by?(v) }
55
+
56
+ raise "Couldn't find Ruby version!" unless ruby_version
57
+
58
+ ruby_version
59
+ end
60
+
61
+ # rubocop:disable Security/Eval
62
+ def ruby_requirement
63
+ ast = Parser::CurrentRuby.parse(gemspec.content)
64
+ requirement_node = find_ruby_requirement_node(ast)
65
+ return unless requirement_node
66
+
67
+ eval(requirement_node.children[2].loc.expression.source)
68
+ end
69
+ # rubocop:enable Security/Eval
70
+
71
+ def find_ruby_requirement_node(node)
72
+ return unless node.is_a?(Parser::AST::Node)
73
+ return node if declares_ruby_requirement?(node)
74
+
75
+ node.children.find do |cn|
76
+ requirement_node = find_ruby_requirement_node(cn)
77
+ break requirement_node if requirement_node
78
+ end
79
+ end
80
+
81
+ def declares_ruby_requirement?(node)
82
+ return false unless node.is_a?(Parser::AST::Node)
83
+
84
+ node.children[1] == :required_ruby_version=
85
+ end
86
+
87
+ class GemfileRewriter < Parser::TreeRewriter
88
+ def initialize(ruby_version:)
89
+ @ruby_version = ruby_version
90
+ end
91
+
92
+ def on_send(node)
93
+ return unless declares_ruby_version?(node)
94
+
95
+ assigned_version_node = node.children[2]
96
+ replace(assigned_version_node.loc.expression, "'#{ruby_version}'")
97
+ end
98
+
99
+ private
100
+
101
+ attr_reader :ruby_version
102
+
103
+ def declares_ruby_version?(node)
104
+ return false unless node.is_a?(Parser::AST::Node)
105
+ return false unless node.type == :send
106
+
107
+ node.children[1] == :ruby
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,244 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/monkey_patches/bundler/definition_ruby_version_patch"
4
+ require "dependabot/monkey_patches/bundler/definition_bundler_version_patch"
5
+ require "dependabot/monkey_patches/bundler/git_source_patch"
6
+
7
+ require "excon"
8
+
9
+ require "dependabot/bundler/update_checker"
10
+ require "dependabot/shared_helpers"
11
+ require "dependabot/errors"
12
+
13
+ module Dependabot
14
+ module Bundler
15
+ class UpdateChecker
16
+ module SharedBundlerHelpers
17
+ GIT_REGEX = /reset --hard [^\s]*` in directory (?<path>[^\s]*)/.freeze
18
+ GIT_REF_REGEX = /not exist in the repository (?<path>[^\s]*)\./.freeze
19
+ PATH_REGEX = /The path `(?<path>.*)` does not exist/.freeze
20
+ RETRYABLE_ERRORS = %w(
21
+ Bundler::HTTPError
22
+ Bundler::Fetcher::FallbackError
23
+ ).freeze
24
+ RETRYABLE_PRIVATE_REGISTRY_ERRORS = %w(
25
+ Bundler::GemNotFound
26
+ Gem::InvalidSpecificationException
27
+ Bundler::VersionConflict
28
+ Bundler::HTTPError
29
+ Bundler::Fetcher::FallbackError
30
+ ).freeze
31
+
32
+ attr_reader :dependency_files, :credentials
33
+
34
+ #########################
35
+ # Bundler context setup #
36
+ #########################
37
+
38
+ def in_a_temporary_bundler_context(error_handling: true)
39
+ base_directory = dependency_files.first.directory
40
+ SharedHelpers.in_a_temporary_directory(base_directory) do |tmp_dir|
41
+ write_temporary_dependency_files
42
+
43
+ SharedHelpers.in_a_forked_process do
44
+ # Set the path for path gemspec correctly
45
+ ::Bundler.instance_variable_set(:@root, tmp_dir)
46
+
47
+ # Remove installed gems from the default Rubygems index
48
+ ::Gem::Specification.all = []
49
+
50
+ # Set auth details
51
+ relevant_credentials.each do |cred|
52
+ token = cred["token"] ||
53
+ "#{cred['username']}:#{cred['password']}"
54
+
55
+ ::Bundler.settings.set_command_option(
56
+ cred.fetch("host"),
57
+ token.gsub("@", "%40F").gsub("?", "%3F")
58
+ )
59
+ end
60
+
61
+ yield
62
+ end
63
+ end
64
+ rescue SharedHelpers::ChildProcessFailed => error
65
+ retry_count ||= 0
66
+ retry_count += 1
67
+ if retryable_error?(error) && retry_count <= 2
68
+ sleep(rand(1.0..5.0)) && retry
69
+ end
70
+
71
+ raise unless error_handling
72
+
73
+ # Raise more descriptive errors
74
+ handle_bundler_errors(error)
75
+ end
76
+
77
+ def retryable_error?(error)
78
+ return true if RETRYABLE_ERRORS.include?(error.error_class)
79
+
80
+ unless RETRYABLE_PRIVATE_REGISTRY_ERRORS.include?(error.error_class)
81
+ return false
82
+ end
83
+
84
+ private_registry_credentials.any?
85
+ end
86
+
87
+ # rubocop:disable Metrics/CyclomaticComplexity
88
+ # rubocop:disable Metrics/PerceivedComplexity
89
+ # rubocop:disable Metrics/AbcSize
90
+ # rubocop:disable Metrics/MethodLength
91
+ def handle_bundler_errors(error)
92
+ msg = error.error_class + " with message: " + error.error_message
93
+
94
+ case error.error_class
95
+ when "Bundler::Dsl::DSLError", "Bundler::GemspecError"
96
+ # We couldn't evaluate the Gemfile, let alone resolve it
97
+ raise Dependabot::DependencyFileNotEvaluatable, msg
98
+ when "Bundler::Source::Git::MissingGitRevisionError"
99
+ gem_name =
100
+ error.error_message.match(GIT_REF_REGEX).
101
+ named_captures["path"].
102
+ split("/").last
103
+ raise GitDependencyReferenceNotFound, gem_name
104
+ when "Bundler::PathError"
105
+ gem_name =
106
+ error.error_message.match(PATH_REGEX).
107
+ named_captures["path"].
108
+ split("/").last.split("-")[0..-2].join
109
+ raise Dependabot::PathDependenciesNotReachable, [gem_name]
110
+ when "Bundler::Source::Git::GitCommandError"
111
+ if error.error_message.match?(GIT_REGEX)
112
+ # We couldn't find the specified branch / commit (or the two
113
+ # weren't compatible).
114
+ gem_name =
115
+ error.error_message.match(GIT_REGEX).
116
+ named_captures["path"].
117
+ split("/").last.split("-")[0..-2].join
118
+ raise GitDependencyReferenceNotFound, gem_name
119
+ end
120
+
121
+ bad_uris = inaccessible_git_dependencies.map { |s| s.source.uri }
122
+ raise unless bad_uris.any?
123
+
124
+ # We don't have access to one of repos required
125
+ raise Dependabot::GitDependenciesNotReachable, bad_uris
126
+ when "Bundler::GemNotFound", "Gem::InvalidSpecificationException",
127
+ "Bundler::VersionConflict"
128
+ # Bundler threw an error during resolution. Any of:
129
+ # - the gem doesn't exist in any of the specified sources
130
+ # - the gem wasn't specified properly
131
+ # - the gem was specified at an incompatible version
132
+ raise Dependabot::DependencyFileNotResolvable, msg
133
+ when "Bundler::Fetcher::AuthenticationRequiredError"
134
+ regex = /bundle config (?<source>.*) username:password/
135
+ source = error.error_message.match(regex)[:source]
136
+ raise Dependabot::PrivateSourceAuthenticationFailure, source
137
+ when "Bundler::Fetcher::BadAuthenticationError"
138
+ regex = /Bad username or password for (?<source>.*)\.$/
139
+ source = error.error_message.match(regex)[:source]
140
+ raise Dependabot::PrivateSourceAuthenticationFailure, source
141
+ when "Bundler::Fetcher::CertificateFailureError"
142
+ regex = /verify the SSL certificate for (?<source>.*)\.$/
143
+ source = error.error_message.match(regex)[:source]
144
+ raise Dependabot::PrivateSourceCertificateFailure, source
145
+ when "Bundler::HTTPError"
146
+ regex = /Could not fetch specs from (?<source>.*)$/
147
+ if error.error_message.match?(regex)
148
+ source = error.error_message.match(regex)[:source]
149
+ raise if source.include?("rubygems.org")
150
+
151
+ raise Dependabot::PrivateSourceTimedOut, source
152
+ end
153
+
154
+ # JFrog can serve a 403 if the credentials provided are good but
155
+ # don't have access to a particular gem.
156
+ raise unless error.error_message.include?("permitted to deploy")
157
+ raise unless jfrog_source
158
+
159
+ raise Dependabot::PrivateSourceAuthenticationFailure, jfrog_source
160
+ else raise
161
+ end
162
+ end
163
+ # rubocop:enable Metrics/CyclomaticComplexity
164
+ # rubocop:enable Metrics/PerceivedComplexity
165
+ # rubocop:enable Metrics/AbcSize
166
+ # rubocop:enable Metrics/MethodLength
167
+
168
+ def inaccessible_git_dependencies
169
+ in_a_temporary_bundler_context(error_handling: false) do
170
+ ::Bundler::Definition.build(gemfile.name, nil, {}).dependencies.
171
+ reject do |spec|
172
+ next true unless spec.source.is_a?(::Bundler::Source::Git)
173
+
174
+ # Piggy-back off some private Bundler methods to configure the
175
+ # URI with auth details in the same way Bundler does.
176
+ git_proxy = spec.source.send(:git_proxy)
177
+ uri = spec.source.uri.gsub("git://", "https://")
178
+ uri = git_proxy.send(:configured_uri_for, uri)
179
+ uri += ".git" unless uri.end_with?(".git")
180
+ uri += "/info/refs?service=git-upload-pack"
181
+
182
+ begin
183
+ Excon.get(
184
+ uri,
185
+ idempotent: true,
186
+ **SharedHelpers.excon_defaults
187
+ ).status == 200
188
+ rescue Excon::Error::Socket, Excon::Error::Timeout
189
+ false
190
+ end
191
+ end
192
+ end
193
+ end
194
+
195
+ def jfrog_source
196
+ in_a_temporary_bundler_context(error_handling: false) do
197
+ ::Bundler::Definition.build(gemfile.name, nil, {}).
198
+ send(:sources).
199
+ rubygems_remotes.
200
+ find { |uri| uri.host.include?("jfrog") }&.
201
+ host
202
+ end
203
+ end
204
+
205
+ def write_temporary_dependency_files
206
+ dependency_files.each do |file|
207
+ path = file.name
208
+ FileUtils.mkdir_p(Pathname.new(path).dirname)
209
+ File.write(path, file.content)
210
+ end
211
+
212
+ File.write(lockfile.name, sanitized_lockfile_body) if lockfile
213
+ end
214
+
215
+ def relevant_credentials
216
+ private_registry_credentials + git_source_credentials
217
+ end
218
+
219
+ def private_registry_credentials
220
+ credentials.select { |cred| cred["type"] == "rubygems_server" }
221
+ end
222
+
223
+ def git_source_credentials
224
+ credentials.select { |cred| cred["type"] == "git_source" }
225
+ end
226
+
227
+ def gemfile
228
+ dependency_files.find { |f| f.name == "Gemfile" } ||
229
+ dependency_files.find { |f| f.name == "gems.rb" }
230
+ end
231
+
232
+ def lockfile
233
+ dependency_files.find { |f| f.name == "Gemfile.lock" } ||
234
+ dependency_files.find { |f| f.name == "gems.locked" }
235
+ end
236
+
237
+ def sanitized_lockfile_body
238
+ re = FileUpdater::LockfileUpdater::LOCKFILE_ENDING
239
+ lockfile.content.gsub(re, "")
240
+ end
241
+ end
242
+ end
243
+ end
244
+ end
@@ -0,0 +1,272 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dependabot/monkey_patches/bundler/definition_ruby_version_patch"
4
+ require "dependabot/monkey_patches/bundler/definition_bundler_version_patch"
5
+ require "dependabot/monkey_patches/bundler/git_source_patch"
6
+
7
+ require "excon"
8
+
9
+ require "dependabot/bundler/update_checker"
10
+ require "dependabot/bundler/file_updater/lockfile_updater"
11
+ require "dependabot/bundler/requirement"
12
+ require "dependabot/shared_helpers"
13
+ require "dependabot/errors"
14
+
15
+ module Dependabot
16
+ module Bundler
17
+ class UpdateChecker
18
+ class VersionResolver
19
+ require_relative "file_preparer"
20
+ require_relative "latest_version_finder"
21
+ require_relative "shared_bundler_helpers"
22
+ include SharedBundlerHelpers
23
+
24
+ GEM_NOT_FOUND_ERROR_REGEX = /locked to (?<name>[^\s]+) \(/.freeze
25
+
26
+ def initialize(dependency:, unprepared_dependency_files:,
27
+ credentials:, ignored_versions:,
28
+ replacement_git_pin: nil, remove_git_source: false,
29
+ unlock_requirement: true,
30
+ latest_allowable_version: nil)
31
+ @dependency = dependency
32
+ @unprepared_dependency_files = unprepared_dependency_files
33
+ @credentials = credentials
34
+ @ignored_versions = ignored_versions
35
+ @replacement_git_pin = replacement_git_pin
36
+ @remove_git_source = remove_git_source
37
+ @unlock_requirement = unlock_requirement
38
+ @latest_allowable_version = latest_allowable_version
39
+ end
40
+
41
+ def latest_resolvable_version_details
42
+ @latest_resolvable_version_details ||=
43
+ fetch_latest_resolvable_version_details
44
+ end
45
+
46
+ private
47
+
48
+ attr_reader :dependency, :unprepared_dependency_files, :credentials,
49
+ :ignored_versions, :replacement_git_pin,
50
+ :latest_allowable_version
51
+
52
+ def remove_git_source?
53
+ @remove_git_source
54
+ end
55
+
56
+ def unlock_requirement?
57
+ @unlock_requirement
58
+ end
59
+
60
+ def dependency_files
61
+ @dependency_files ||=
62
+ FilePreparer.new(
63
+ dependency: dependency,
64
+ dependency_files: unprepared_dependency_files,
65
+ replacement_git_pin: replacement_git_pin,
66
+ remove_git_source: remove_git_source?,
67
+ unlock_requirement: unlock_requirement?,
68
+ latest_allowable_version: latest_allowable_version
69
+ ).prepared_dependency_files
70
+ end
71
+
72
+ # rubocop:disable Metrics/CyclomaticComplexity
73
+ # rubocop:disable Metrics/PerceivedComplexity
74
+ def fetch_latest_resolvable_version_details
75
+ return latest_version_details unless gemfile
76
+
77
+ SharedHelpers.with_git_configured(credentials: credentials) do
78
+ in_a_temporary_bundler_context do
79
+ dep = dependency_from_definition
80
+
81
+ # If the dependency wasn't found in the definition, but *is*
82
+ # included in a gemspec, it's because the Gemfile didn't import
83
+ # the gemspec. This is unusual, but the correct behaviour if/when
84
+ # it happens is to behave as if the repo was gemspec-only.
85
+ if dep.nil? && dependency.requirements.any?
86
+ next latest_version_details
87
+ end
88
+
89
+ # Otherwise, if the dependency wasn't found it's because it is a
90
+ # subdependency that was removed when attempting to update it.
91
+ next nil if dep.nil?
92
+
93
+ # If the old Gemfile index was used then it won't have checked
94
+ # Ruby compatibility. Fix that by doing the check manually (and
95
+ # saying no update is possible if the Ruby version is a mismatch)
96
+ next nil if ruby_version_incompatible?(dep)
97
+
98
+ details = { version: dep.version }
99
+ if dep.source.instance_of?(::Bundler::Source::Git)
100
+ details[:commit_sha] = dep.source.revision
101
+ end
102
+ details
103
+ end
104
+ end
105
+ rescue Dependabot::DependencyFileNotResolvable => error
106
+ return if ignored_versions.any? && !dependency.appears_in_lockfile?
107
+ raise unless ruby_lock_error?(error)
108
+
109
+ @gemspec_ruby_unlocked = true
110
+ regenerate_dependency_files_without_ruby_lock && retry
111
+ end
112
+ # rubocop:enable Metrics/CyclomaticComplexity
113
+ # rubocop:enable Metrics/PerceivedComplexity
114
+
115
+ def ruby_lock_error?(error)
116
+ return false unless error.message.include?(" for gem \"ruby\0\"")
117
+ return false if @gemspec_ruby_unlocked
118
+
119
+ dependency_files.any? { |f| f.name.end_with?(".gemspec") }
120
+ end
121
+
122
+ def regenerate_dependency_files_without_ruby_lock
123
+ @dependency_files =
124
+ FilePreparer.new(
125
+ dependency: dependency,
126
+ dependency_files: unprepared_dependency_files,
127
+ replacement_git_pin: replacement_git_pin,
128
+ remove_git_source: remove_git_source?,
129
+ unlock_requirement: unlock_requirement?,
130
+ latest_allowable_version: latest_allowable_version,
131
+ lock_ruby_version: false
132
+ ).prepared_dependency_files
133
+ end
134
+
135
+ # rubocop:disable Metrics/CyclomaticComplexity
136
+ # rubocop:disable Metrics/PerceivedComplexity
137
+ def dependency_from_definition(unlock_subdependencies: true)
138
+ dependencies_to_unlock = [dependency.name]
139
+ dependencies_to_unlock += subdependencies if unlock_subdependencies
140
+ begin
141
+ definition = build_definition(dependencies_to_unlock)
142
+ definition.resolve_remotely!
143
+ rescue ::Bundler::GemNotFound => error
144
+ unlock_yanked_gem(dependencies_to_unlock, error) && retry
145
+ rescue ::Bundler::HTTPError => error
146
+ # Retry network errors
147
+ attempt ||= 1
148
+ attempt += 1
149
+ raise if attempt > 3 || !error.message.include?("Network error")
150
+
151
+ retry
152
+ end
153
+
154
+ dep = definition.resolve.find { |d| d.name == dependency.name }
155
+ return dep if dep
156
+ return if dependency.requirements.any? || !unlock_subdependencies
157
+
158
+ # If no definition was found and we're updating a sub-dependency,
159
+ # try again but without unlocking any other sub-dependencies
160
+ dependency_from_definition(unlock_subdependencies: false)
161
+ end
162
+ # rubocop:enable Metrics/CyclomaticComplexity
163
+ # rubocop:enable Metrics/PerceivedComplexity
164
+
165
+ def unlock_yanked_gem(dependencies_to_unlock, error)
166
+ raise unless error.message.match?(GEM_NOT_FOUND_ERROR_REGEX)
167
+
168
+ gem_name = error.message.match(GEM_NOT_FOUND_ERROR_REGEX).
169
+ named_captures["name"]
170
+ raise if dependencies_to_unlock.include?(gem_name)
171
+
172
+ dependencies_to_unlock << gem_name
173
+ end
174
+
175
+ def subdependencies
176
+ # If there's no lockfile we don't need to worry about
177
+ # subdependencies
178
+ return [] unless lockfile
179
+
180
+ all_deps = ::Bundler::LockfileParser.new(sanitized_lockfile_body).
181
+ specs.map(&:name).map(&:to_s)
182
+ top_level = build_definition([]).dependencies.
183
+ map(&:name).map(&:to_s)
184
+
185
+ all_deps - top_level
186
+ end
187
+
188
+ def ruby_version_incompatible?(dep)
189
+ return false unless dep.source.is_a?(::Bundler::Source::Rubygems)
190
+
191
+ fetcher = dep.source.fetchers.first.fetchers.first
192
+
193
+ # It's only the old index we have a problem with
194
+ return false unless fetcher.is_a?(::Bundler::Fetcher::Dependency)
195
+
196
+ # If no Ruby version is specified, we don't have a problem
197
+ return false unless ruby_version
198
+
199
+ versions = Excon.get(
200
+ "#{fetcher.fetch_uri}api/v1/versions/#{dependency.name}.json",
201
+ idempotent: true,
202
+ **SharedHelpers.excon_defaults
203
+ )
204
+
205
+ # Give the benefit of the doubt if something goes wrong fetching
206
+ # version details (could be that it's a private index, etc.)
207
+ return false unless versions.status == 200
208
+
209
+ ruby_requirement =
210
+ JSON.parse(versions.body).
211
+ find { |details| details["number"] == dep.version.to_s }&.
212
+ fetch("ruby_version", nil)
213
+
214
+ # Give the benefit of the doubt if we can't find the version's
215
+ # required Ruby version.
216
+ return false unless ruby_requirement
217
+
218
+ ruby_requirement = Requirement.new(ruby_requirement)
219
+
220
+ !ruby_requirement.satisfied_by?(ruby_version)
221
+ rescue JSON::ParserError, Excon::Error::Socket, Excon::Error::Timeout
222
+ # Give the benefit of the doubt if something goes wrong fetching
223
+ # version details (could be that it's a private index, etc.)
224
+ false
225
+ end
226
+
227
+ def build_definition(dependencies_to_unlock)
228
+ # Note: we lock shared dependencies to avoid any top-level
229
+ # dependencies getting unlocked (which would happen if they were
230
+ # also subdependencies of the dependency being unlocked)
231
+ ::Bundler::Definition.build(
232
+ gemfile.name,
233
+ lockfile&.name,
234
+ gems: dependencies_to_unlock,
235
+ lock_shared_dependencies: true
236
+ )
237
+ end
238
+
239
+ def ruby_version
240
+ return nil unless gemfile
241
+
242
+ @ruby_version ||= build_definition([]).ruby_version&.gem_version
243
+ end
244
+
245
+ def latest_version_details
246
+ @latest_version_details ||=
247
+ LatestVersionFinder.new(
248
+ dependency: dependency,
249
+ dependency_files: dependency_files,
250
+ credentials: credentials,
251
+ ignored_versions: ignored_versions
252
+ ).latest_version_details
253
+ end
254
+
255
+ def gemfile
256
+ dependency_files.find { |f| f.name == "Gemfile" } ||
257
+ dependency_files.find { |f| f.name == "gems.rb" }
258
+ end
259
+
260
+ def lockfile
261
+ dependency_files.find { |f| f.name == "Gemfile.lock" } ||
262
+ dependency_files.find { |f| f.name == "gems.locked" }
263
+ end
264
+
265
+ def sanitized_lockfile_body
266
+ re = FileUpdater::LockfileUpdater::LOCKFILE_ENDING
267
+ lockfile.content.gsub(re, "")
268
+ end
269
+ end
270
+ end
271
+ end
272
+ end