dependabot-bundler 0.129.1 → 0.129.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b239879862978b10704d5772fb74b3df2a77621b507f30918b9caba59fb48a4c
4
- data.tar.gz: e1e3f9b64a99e6740cdeb496450c6a04423987fa6196bd3dacf2a6004b8107a9
3
+ metadata.gz: be36ec3f06ae706aebd65e4cf5445bfa793ab68bf4b61b9611ce2ed52efedf90
4
+ data.tar.gz: 8ab197c0501c20f245ea594a196931b7fa31052d58c93e94bb07c681fbb7d6a2
5
5
  SHA512:
6
- metadata.gz: cc82c28e54933523085591ef557352224d0dcdf2d28c83b2de58d0df341cd6d5bf053e21e2d400a8184d34a5783e5bb2e2ecf530e3a5fb079538f76763e2fa5d
7
- data.tar.gz: 3b743693c65c964babe9a6df82ee56113aed699bcb7eef69c60ac88a650902855d0ed29ba27d21638b79715677422c4e68425998585c94b536125d89575bb2f4
6
+ metadata.gz: acaa6896d65314fc67f0e5b6ec23ec5599b3b5a496b19ac266be7efe3dc7f859003129b9ca740073ea9f33c2b923d401f04045dd8445fc30ae7b142dc768c12b
7
+ data.tar.gz: 690d961c54b72aedfb095da794b5e7bb7cd7b0f134c18129f09c942e898ef2c246fc5f0e137633159af1f781b6f00963ea5ed3484f438fa888ea4f0874d56c26
@@ -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
@@ -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
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.129.1
4
+ version: 0.129.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dependabot
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-12-21 00:00:00.000000000 Z
11
+ date: 2021-01-04 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.129.1
19
+ version: 0.129.2
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.129.1
26
+ version: 0.129.2
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.6.0
103
+ version: 1.7.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.6.0
110
+ version: 1.7.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.20.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.20.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