dependabot-bundler 0.380.0 → 0.381.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/v2/build +2 -2
- data/helpers/v2/lib/bundler_version_constraint.rb +1 -1
- data/helpers/v2/monkey_patches/definition_ruby_version_patch.rb +1 -1
- data/helpers/v2/run.rb +5 -4
- data/helpers/v2/spec/bundler_version_constraint_spec.rb +6 -8
- data/helpers/v4/.gitignore +8 -0
- data/helpers/v4/Gemfile +7 -0
- data/helpers/v4/build +45 -0
- data/helpers/v4/lib/bundler_version_constraint.rb +25 -0
- data/helpers/v4/lib/functions/conflicting_dependency_resolver.rb +87 -0
- data/helpers/v4/lib/functions/dependency_source.rb +90 -0
- data/helpers/v4/lib/functions/file_parser.rb +119 -0
- data/helpers/v4/lib/functions/force_updater.rb +189 -0
- data/helpers/v4/lib/functions/lockfile_updater.rb +234 -0
- data/helpers/v4/lib/functions/version_resolver.rb +145 -0
- data/helpers/v4/lib/functions.rb +187 -0
- data/helpers/v4/monkey_patches/definition_bundler_version_patch.rb +14 -0
- data/helpers/v4/monkey_patches/definition_ruby_version_patch.rb +53 -0
- data/helpers/v4/monkey_patches/git_source_patch.rb +67 -0
- data/helpers/v4/run.rb +46 -0
- data/helpers/v4/spec/bundler_version_constraint_spec.rb +63 -0
- data/helpers/v4/spec/functions/conflicting_dependency_resolver_spec.rb +130 -0
- data/helpers/v4/spec/functions/dependency_source_spec.rb +193 -0
- data/helpers/v4/spec/functions/file_parser_spec.rb +140 -0
- data/helpers/v4/spec/functions/force_updater_spec.rb +59 -0
- data/helpers/v4/spec/functions/version_resolver_spec.rb +118 -0
- data/helpers/v4/spec/functions_spec.rb +59 -0
- data/helpers/v4/spec/native_spec_helper.rb +57 -0
- data/helpers/v4/spec/ruby_version_spec.rb +42 -0
- data/helpers/v4/spec/shared_contexts.rb +51 -0
- data/lib/dependabot/bundler/file_parser.rb +5 -3
- data/lib/dependabot/bundler/helpers.rb +12 -1
- metadata +29 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d6dace53b7c6c93f276ae838f5c2c44f941388b5e129380ba19350ebb7f13eff
|
|
4
|
+
data.tar.gz: ce2a77ec63b75dadeb2eda06b97c443b92130d968b70f907de766b7749351901
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fa101fe9112a94de4a65f83570bbd57343e0a90ca0f2a66e178752c9b05ec604d352f4a11712e3832afb5d2ab699147c100894bfb971da9bed6a397f7c4417cb
|
|
7
|
+
data.tar.gz: f0ee43f8880fc22c0b8521c5290dfd46575c663c133bedde28b41e51f5ebb7ad81b519e96a28ce621b88395822df6a57ccf465cd73cd7b726205f612f1b31ed3
|
data/helpers/v2/build
CHANGED
|
@@ -19,8 +19,8 @@ fi
|
|
|
19
19
|
|
|
20
20
|
cd "$install_dir"
|
|
21
21
|
|
|
22
|
-
# Default to Bundler
|
|
23
|
-
bundler_constraint="${DEPENDABOT_BUNDLER_VERSION_CONSTRAINT:-${BUNDLER_VERSION_CONSTRAINT:-~>
|
|
22
|
+
# Default to Bundler 2, with an override for controlled testing/rollouts.
|
|
23
|
+
bundler_constraint="${DEPENDABOT_BUNDLER_VERSION_CONSTRAINT:-${BUNDLER_VERSION_CONSTRAINT:-~> 2}}"
|
|
24
24
|
|
|
25
25
|
export GEM_HOME=$install_dir/.bundle
|
|
26
26
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
# Used by both `run.rb` (for activation via `gem`) and the helper specs so
|
|
9
9
|
# the rollback/staged-rollout behavior is exercised by real code.
|
|
10
10
|
module BundlerVersionConstraint
|
|
11
|
-
DEFAULT_ACTIVATION_CONSTRAINT = ">= 2.4, <
|
|
11
|
+
DEFAULT_ACTIVATION_CONSTRAINT = ">= 2.4, < 3"
|
|
12
12
|
|
|
13
13
|
def self.resolve(env: ENV, default: DEFAULT_ACTIVATION_CONSTRAINT)
|
|
14
14
|
env.fetch(
|
|
@@ -26,7 +26,7 @@ module BundlerDefinitionRubyVersionPatch
|
|
|
26
26
|
Gem::Specification.new("Ruby\0", requested_version)
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
%w(2.5.3 2.6.10 2.7.8 3.0.7 3.1.6 3.2.8 3.3.8).each do |version|
|
|
29
|
+
%w(2.5.3 2.6.10 2.7.8 3.0.7 3.1.6 3.2.8 3.3.8 3.4.8).each do |version|
|
|
30
30
|
sources.metadata_source.specs << Gem::Specification.new("Ruby\0", version)
|
|
31
31
|
end
|
|
32
32
|
|
data/helpers/v2/run.rb
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
|
|
4
4
|
require_relative "lib/bundler_version_constraint"
|
|
5
5
|
|
|
6
|
-
#
|
|
7
|
-
# major versions
|
|
8
|
-
#
|
|
9
|
-
#
|
|
6
|
+
# Activate Bundler 2 by default with an upper bound to prevent unintended
|
|
7
|
+
# future major versions (Bundler 4 lives in the v4 helper tree). Honor
|
|
8
|
+
# DEPENDABOT_BUNDLER_VERSION_CONSTRAINT (or its BUNDLER_VERSION_CONSTRAINT
|
|
9
|
+
# fallback) so staged rollouts and emergency rollbacks performed by the build
|
|
10
|
+
# script are respected at activation time.
|
|
10
11
|
bundler_constraint = BundlerVersionConstraint.resolve
|
|
11
12
|
gem "bundler", *BundlerVersionConstraint.activation_clauses(bundler_constraint)
|
|
12
13
|
require "bundler"
|
|
@@ -6,12 +6,10 @@ require_relative "../lib/bundler_version_constraint"
|
|
|
6
6
|
|
|
7
7
|
RSpec.describe BundlerVersionConstraint do
|
|
8
8
|
describe "helper runtime activation" do
|
|
9
|
-
it "is running
|
|
10
|
-
#
|
|
11
|
-
# straight to 4.0 to align with RubyGems) so the supported window is
|
|
12
|
-
# 2.x or 4.x.
|
|
9
|
+
it "is running Bundler 2" do
|
|
10
|
+
# The v2 helper tree pins Bundler 2.x; Bundler 4 lives in the v4 tree.
|
|
13
11
|
bundler_major = Bundler::VERSION.split(".").first.to_i
|
|
14
|
-
expect(bundler_major).to
|
|
12
|
+
expect(bundler_major).to eq(2)
|
|
15
13
|
end
|
|
16
14
|
end
|
|
17
15
|
|
|
@@ -35,7 +33,7 @@ RSpec.describe BundlerVersionConstraint do
|
|
|
35
33
|
end
|
|
36
34
|
|
|
37
35
|
it "uses the default activation constraint when no env var is set" do
|
|
38
|
-
expect(described_class.resolve(env: {})).to eq(">= 2.4, <
|
|
36
|
+
expect(described_class.resolve(env: {})).to eq(">= 2.4, < 3")
|
|
39
37
|
end
|
|
40
38
|
|
|
41
39
|
it "honours an explicit default override" do
|
|
@@ -45,11 +43,11 @@ RSpec.describe BundlerVersionConstraint do
|
|
|
45
43
|
|
|
46
44
|
describe ".activation_clauses" do
|
|
47
45
|
it "splits comma-separated requirement strings into trimmed clauses" do
|
|
48
|
-
expect(described_class.activation_clauses(">= 2.4, <
|
|
46
|
+
expect(described_class.activation_clauses(">= 2.4, < 3")).to eq([">= 2.4", "< 3"])
|
|
49
47
|
end
|
|
50
48
|
|
|
51
49
|
it "returns a single clause for a single requirement" do
|
|
52
|
-
expect(described_class.activation_clauses("~>
|
|
50
|
+
expect(described_class.activation_clauses("~> 2")).to eq(["~> 2"])
|
|
53
51
|
end
|
|
54
52
|
end
|
|
55
53
|
|
data/helpers/v4/Gemfile
ADDED
data/helpers/v4/build
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -e
|
|
4
|
+
|
|
5
|
+
helpers_dir=$(cd -P "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
6
|
+
|
|
7
|
+
if [ -z "$DEPENDABOT_NATIVE_HELPERS_PATH" ]; then
|
|
8
|
+
install_dir="$helpers_dir"
|
|
9
|
+
else
|
|
10
|
+
install_dir="$DEPENDABOT_NATIVE_HELPERS_PATH/bundler/v4"
|
|
11
|
+
mkdir -p "$install_dir"
|
|
12
|
+
|
|
13
|
+
cp -r \
|
|
14
|
+
"$helpers_dir/lib" \
|
|
15
|
+
"$helpers_dir/monkey_patches" \
|
|
16
|
+
"$helpers_dir/run.rb" \
|
|
17
|
+
"$install_dir"
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
cd "$install_dir"
|
|
21
|
+
|
|
22
|
+
# Default to Bundler 4, with an override for controlled testing/rollouts.
|
|
23
|
+
bundler_constraint="${DEPENDABOT_BUNDLER_VERSION_CONSTRAINT:-${BUNDLER_VERSION_CONSTRAINT:-~> 4.0}}"
|
|
24
|
+
|
|
25
|
+
export GEM_HOME=$install_dir/.bundle
|
|
26
|
+
|
|
27
|
+
gem install bundler -v "$bundler_constraint" --no-document
|
|
28
|
+
|
|
29
|
+
# Resolve the Bundler version that was actually installed in GEM_HOME to ensure
|
|
30
|
+
# consistency with what was requested and to avoid picking up system gems.
|
|
31
|
+
default_version=$(ruby -e '
|
|
32
|
+
gemspecs = Dir.glob("#{ENV["GEM_HOME"]}/specifications/bundler-*.gemspec")
|
|
33
|
+
latest = gemspecs.max_by { |f| Gem::Version.new(File.basename(f).match(/bundler-(.*)\.gemspec/)[1]) }
|
|
34
|
+
abort("No bundler gemspec found in #{ENV["GEM_HOME"]}/specifications") unless latest
|
|
35
|
+
print File.basename(latest).match(/bundler-(.*)\.gemspec/)[1]
|
|
36
|
+
')
|
|
37
|
+
|
|
38
|
+
if [ -z "$default_version" ]; then
|
|
39
|
+
echo "error: failed to resolve installed Bundler version in $GEM_HOME" >&2
|
|
40
|
+
exit 1
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
if [ -z "$DEPENDABOT_NATIVE_HELPERS_PATH" ]; then
|
|
44
|
+
bundle _"$default_version"_ install
|
|
45
|
+
fi
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Resolves the Bundler version constraint that the native helper should use
|
|
5
|
+
# at activation time. Honors DEPENDABOT_BUNDLER_VERSION_CONSTRAINT, falling
|
|
6
|
+
# back to BUNDLER_VERSION_CONSTRAINT, and finally to the supplied default.
|
|
7
|
+
#
|
|
8
|
+
# Used by both `run.rb` (for activation via `gem`) and the helper specs so
|
|
9
|
+
# the rollback/staged-rollout behavior is exercised by real code.
|
|
10
|
+
module BundlerVersionConstraint
|
|
11
|
+
DEFAULT_ACTIVATION_CONSTRAINT = ">= 4, < 5"
|
|
12
|
+
|
|
13
|
+
def self.resolve(env: ENV, default: DEFAULT_ACTIVATION_CONSTRAINT)
|
|
14
|
+
env.fetch(
|
|
15
|
+
"DEPENDABOT_BUNDLER_VERSION_CONSTRAINT",
|
|
16
|
+
env.fetch("BUNDLER_VERSION_CONSTRAINT", default)
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Splits a comma-separated requirement string into the individual clauses
|
|
21
|
+
# accepted by Kernel#gem (e.g. ">= 2.4, < 5" -> [">= 2.4", "< 5"]).
|
|
22
|
+
def self.activation_clauses(constraint)
|
|
23
|
+
constraint.split(",").map(&:strip)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Functions
|
|
5
|
+
class ConflictingDependencyResolver
|
|
6
|
+
def initialize(dependency_name:, target_version:, lockfile_name:)
|
|
7
|
+
@dependency_name = dependency_name
|
|
8
|
+
@target_version = target_version
|
|
9
|
+
@lockfile_name = lockfile_name
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Finds any dependencies in the lockfile that have a subdependency on the
|
|
13
|
+
# given dependency that does not satisfly the target_version.
|
|
14
|
+
# @return [Array<Hash{String => String}]
|
|
15
|
+
# * explanation [String] a sentence explaining the conflict
|
|
16
|
+
# * name [String] the blocking dependencies name
|
|
17
|
+
# * version [String] the version of the blocking dependency
|
|
18
|
+
# * requirement [String] the requirement on the target_dependency
|
|
19
|
+
def conflicting_dependencies
|
|
20
|
+
parent_specs.flat_map do |parent_spec|
|
|
21
|
+
top_level_specs_for(parent_spec).map do |top_level|
|
|
22
|
+
dependency = parent_spec.dependencies.find { |bd| bd.name == dependency_name }
|
|
23
|
+
{
|
|
24
|
+
"explanation" => explanation(parent_spec, dependency, top_level),
|
|
25
|
+
"name" => parent_spec.name,
|
|
26
|
+
"version" => parent_spec.version.to_s,
|
|
27
|
+
"requirement" => dependency.requirement.to_s
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
attr_reader :dependency_name
|
|
36
|
+
attr_reader :target_version
|
|
37
|
+
attr_reader :lockfile_name
|
|
38
|
+
|
|
39
|
+
def parent_specs
|
|
40
|
+
version = Gem::Version.new(target_version)
|
|
41
|
+
parsed_lockfile.specs.filter do |spec|
|
|
42
|
+
spec.dependencies.any? do |dep|
|
|
43
|
+
dep.name == dependency_name &&
|
|
44
|
+
!dep.requirement.satisfied_by?(version)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def top_level_specs_for(parent_spec)
|
|
50
|
+
return [parent_spec] if top_level?(parent_spec)
|
|
51
|
+
|
|
52
|
+
parsed_lockfile.specs.filter do |spec|
|
|
53
|
+
spec.dependencies.any? do |dep|
|
|
54
|
+
dep.name == parent_spec.name && top_level?(spec)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def top_level?(spec)
|
|
60
|
+
parsed_lockfile.dependencies.key?(spec.name)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def explanation(spec, dependency, top_level)
|
|
64
|
+
if spec.name == top_level.name
|
|
65
|
+
"#{spec.name} (#{spec.version}) requires #{dependency_name} (#{dependency.requirement})"
|
|
66
|
+
else
|
|
67
|
+
"#{top_level.name} (#{top_level.version}) requires #{dependency_name} " \
|
|
68
|
+
"(#{dependency.requirement}) via #{spec.name} (#{spec.version})"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def parsed_lockfile
|
|
73
|
+
@parsed_lockfile ||= Bundler::LockfileParser.new(lockfile)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def lockfile
|
|
77
|
+
return @lockfile if defined?(@lockfile)
|
|
78
|
+
|
|
79
|
+
@lockfile =
|
|
80
|
+
begin
|
|
81
|
+
return unless lockfile_name && File.exist?(lockfile_name)
|
|
82
|
+
|
|
83
|
+
File.read(lockfile_name)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Functions
|
|
5
|
+
class DependencySource
|
|
6
|
+
attr_reader :gemfile_name
|
|
7
|
+
attr_reader :dependency_name
|
|
8
|
+
|
|
9
|
+
RUBYGEMS = "rubygems"
|
|
10
|
+
PRIVATE_REGISTRY = "private"
|
|
11
|
+
GIT = "git"
|
|
12
|
+
OTHER = "other"
|
|
13
|
+
|
|
14
|
+
def initialize(gemfile_name:, dependency_name:)
|
|
15
|
+
@gemfile_name = gemfile_name
|
|
16
|
+
@dependency_name = dependency_name
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def type
|
|
20
|
+
bundler_source = specified_source || default_source
|
|
21
|
+
type_of(bundler_source)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def latest_git_version(dependency_source_url:, dependency_source_branch:)
|
|
25
|
+
source = Bundler::Source::Git.new(
|
|
26
|
+
"uri" => dependency_source_url,
|
|
27
|
+
"branch" => dependency_source_branch,
|
|
28
|
+
"name" => dependency_name,
|
|
29
|
+
"submodules" => true
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Tell Bundler we're fine with fetching the source remotely
|
|
33
|
+
source.instance_variable_set(:@allow_remote, true)
|
|
34
|
+
|
|
35
|
+
spec = source.specs.first
|
|
36
|
+
{ version: spec.version, commit_sha: spec.source.revision }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def private_registry_versions
|
|
40
|
+
bundler_source = specified_source || default_source
|
|
41
|
+
|
|
42
|
+
bundler_source
|
|
43
|
+
.fetchers.flat_map do |fetcher|
|
|
44
|
+
index = fetcher.specs([dependency_name], bundler_source)
|
|
45
|
+
# Bundler 4 removed Index#search_all; use #search which returns all matches
|
|
46
|
+
specs = index.respond_to?(:search_all) ? index.search_all(dependency_name) : index.search(dependency_name)
|
|
47
|
+
specs.map(&:version)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def type_of(bundler_source)
|
|
54
|
+
case bundler_source
|
|
55
|
+
when Bundler::Source::Rubygems
|
|
56
|
+
remote = bundler_source.remotes.first
|
|
57
|
+
if remote.nil? || remote.to_s == "https://rubygems.org/"
|
|
58
|
+
RUBYGEMS
|
|
59
|
+
else
|
|
60
|
+
PRIVATE_REGISTRY
|
|
61
|
+
end
|
|
62
|
+
when Bundler::Source::Git
|
|
63
|
+
GIT
|
|
64
|
+
else
|
|
65
|
+
OTHER
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def specified_source
|
|
70
|
+
return @specified_source if defined? @specified_source
|
|
71
|
+
|
|
72
|
+
@specified_source = definition.dependencies
|
|
73
|
+
.find { |dep| dep.name == dependency_name }&.source
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def default_source
|
|
77
|
+
definition.send(:sources).default_source
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def definition
|
|
81
|
+
@definition ||= Bundler::Definition.build(gemfile_name, nil, {})
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def serialize_bundler_source(source)
|
|
85
|
+
{
|
|
86
|
+
type: source.class.to_s
|
|
87
|
+
}
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module Functions
|
|
7
|
+
class FileParser
|
|
8
|
+
def initialize(lockfile_name:)
|
|
9
|
+
@lockfile_name = lockfile_name
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
attr_reader :lockfile_name
|
|
13
|
+
|
|
14
|
+
def parsed_gemfile(gemfile_name:)
|
|
15
|
+
Bundler::Definition.build(gemfile_name, nil, {})
|
|
16
|
+
.dependencies.select(&:current_platform?)
|
|
17
|
+
.reject { |dep| local_sources.include?(dep.source.class) }
|
|
18
|
+
.map { |dep| serialize_bundler_dependency(dep) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def parsed_gemspec(gemspec_name:)
|
|
22
|
+
Bundler.load_gemspec_uncached(gemspec_name)
|
|
23
|
+
.dependencies
|
|
24
|
+
.map { |dep| serialize_bundler_dependency(dep) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def lockfile
|
|
30
|
+
return @lockfile if defined?(@lockfile)
|
|
31
|
+
|
|
32
|
+
@lockfile =
|
|
33
|
+
begin
|
|
34
|
+
return unless lockfile_name && File.exist?(lockfile_name)
|
|
35
|
+
|
|
36
|
+
File.read(lockfile_name)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def parsed_lockfile
|
|
41
|
+
return unless lockfile
|
|
42
|
+
|
|
43
|
+
@parsed_lockfile ||= Bundler::LockfileParser.new(lockfile)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def source_from_lockfile(dependency_name)
|
|
47
|
+
parsed_lockfile&.specs&.find { |s| s.name == dependency_name }&.source
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def source_for(dependency)
|
|
51
|
+
source = dependency.source
|
|
52
|
+
if lockfile && default_rubygems?(source)
|
|
53
|
+
# If there's a lockfile and the Gemfile doesn't have anything
|
|
54
|
+
# interesting to say about the source, check that.
|
|
55
|
+
source = source_from_lockfile(dependency.name)
|
|
56
|
+
end
|
|
57
|
+
raise "Bad source: #{source}" unless sources.include?(source.class)
|
|
58
|
+
|
|
59
|
+
return nil if default_rubygems?(source)
|
|
60
|
+
|
|
61
|
+
details = { type: source.class.name.split("::").last.downcase }
|
|
62
|
+
details.merge!(git_source_details(source)) if source.is_a?(Bundler::Source::Git)
|
|
63
|
+
details[:url] = source.remotes.first.to_s if source.is_a?(Bundler::Source::Rubygems)
|
|
64
|
+
details
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def git_source_details(source)
|
|
68
|
+
{
|
|
69
|
+
url: source.uri,
|
|
70
|
+
branch: source.branch,
|
|
71
|
+
ref: source.ref
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
RUBYGEMS_HOSTS = [
|
|
76
|
+
"rubygems.org",
|
|
77
|
+
"www.rubygems.org"
|
|
78
|
+
].freeze
|
|
79
|
+
|
|
80
|
+
def default_rubygems?(source)
|
|
81
|
+
return true if source.nil?
|
|
82
|
+
return false unless source.is_a?(Bundler::Source::Rubygems)
|
|
83
|
+
|
|
84
|
+
source.remotes.any? do |r|
|
|
85
|
+
RUBYGEMS_HOSTS.include?(URI(r.to_s).host)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def serialize_bundler_dependency(dependency)
|
|
90
|
+
{
|
|
91
|
+
name: dependency.name,
|
|
92
|
+
requirement: dependency.requirement,
|
|
93
|
+
groups: dependency.groups,
|
|
94
|
+
source: source_for(dependency),
|
|
95
|
+
type: dependency.type
|
|
96
|
+
}
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Can't be a constant because some of these don't exist in bundler
|
|
100
|
+
# 1.15, which used to cause issues on Heroku (causing exception on boot).
|
|
101
|
+
# TODO: Check if this will be an issue with multiple bundler versions
|
|
102
|
+
def sources
|
|
103
|
+
[
|
|
104
|
+
NilClass,
|
|
105
|
+
Bundler::Source::Rubygems,
|
|
106
|
+
Bundler::Source::Git,
|
|
107
|
+
*local_sources,
|
|
108
|
+
Bundler::Source::Metadata
|
|
109
|
+
]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def local_sources
|
|
113
|
+
[
|
|
114
|
+
Bundler::Source::Path,
|
|
115
|
+
Bundler::Source::Gemspec
|
|
116
|
+
]
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Functions
|
|
5
|
+
class ForceUpdater
|
|
6
|
+
class TopLevelDependencyDowngradedError < StandardError; end
|
|
7
|
+
|
|
8
|
+
def initialize(
|
|
9
|
+
dependency_name:,
|
|
10
|
+
target_version:,
|
|
11
|
+
gemfile_name:,
|
|
12
|
+
lockfile_name:,
|
|
13
|
+
update_multiple_dependencies:
|
|
14
|
+
)
|
|
15
|
+
@dependency_name = dependency_name
|
|
16
|
+
@target_version = target_version
|
|
17
|
+
@gemfile_name = gemfile_name
|
|
18
|
+
@lockfile_name = lockfile_name
|
|
19
|
+
@update_multiple_dependencies = update_multiple_dependencies
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def run
|
|
23
|
+
dependencies_to_unlock = []
|
|
24
|
+
|
|
25
|
+
begin
|
|
26
|
+
definition = build_definition(dependencies_to_unlock: dependencies_to_unlock)
|
|
27
|
+
definition.resolve_remotely!
|
|
28
|
+
specs = definition.resolve
|
|
29
|
+
updates = ([dependency_name, *dependencies_to_unlock] - subdependencies + extra_top_level_deps(specs)).uniq
|
|
30
|
+
|
|
31
|
+
updates = updates.map do |name|
|
|
32
|
+
{
|
|
33
|
+
name: name
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
specs = specs.map do |dep|
|
|
38
|
+
{
|
|
39
|
+
name: dep.name,
|
|
40
|
+
version: dep.version
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
[updates, specs]
|
|
45
|
+
rescue Bundler::SolveFailure => e
|
|
46
|
+
raise unless update_multiple_dependencies?
|
|
47
|
+
|
|
48
|
+
# TODO: Not sure this won't unlock way too many things...
|
|
49
|
+
new_dependencies_to_unlock =
|
|
50
|
+
new_dependencies_to_unlock_from(
|
|
51
|
+
error: e,
|
|
52
|
+
already_unlocked: dependencies_to_unlock
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
raise if new_dependencies_to_unlock.none?
|
|
56
|
+
|
|
57
|
+
dependencies_to_unlock |= new_dependencies_to_unlock
|
|
58
|
+
retry
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
attr_reader :dependency_name
|
|
65
|
+
attr_reader :target_version
|
|
66
|
+
attr_reader :gemfile_name
|
|
67
|
+
attr_reader :lockfile_name
|
|
68
|
+
attr_reader :credentials
|
|
69
|
+
attr_reader :update_multiple_dependencies
|
|
70
|
+
alias update_multiple_dependencies? update_multiple_dependencies
|
|
71
|
+
|
|
72
|
+
def extra_top_level_deps(specs)
|
|
73
|
+
top_level_dep_names.reject do |name|
|
|
74
|
+
original_version = original_specs.find { |s| s.name == name }&.version
|
|
75
|
+
new_version = specs[name].first&.version
|
|
76
|
+
|
|
77
|
+
if original_version == new_version
|
|
78
|
+
true
|
|
79
|
+
else
|
|
80
|
+
original_version = Gem::Version.new(original_version)
|
|
81
|
+
new_version = Gem::Version.new(new_version)
|
|
82
|
+
|
|
83
|
+
raise TopLevelDependencyDowngradedError if new_version < original_version
|
|
84
|
+
|
|
85
|
+
false
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def new_dependencies_to_unlock_from(error:, already_unlocked:)
|
|
91
|
+
names = [*already_unlocked, dependency_name]
|
|
92
|
+
extra_names_to_unlock = []
|
|
93
|
+
|
|
94
|
+
incompatibility = error.cause.incompatibility
|
|
95
|
+
|
|
96
|
+
while incompatibility.conflict?
|
|
97
|
+
cause = incompatibility.cause
|
|
98
|
+
incompatibility = cause.incompatibility
|
|
99
|
+
|
|
100
|
+
incompatibility.terms.each do |term|
|
|
101
|
+
name = term.package.name
|
|
102
|
+
extra_names_to_unlock << name unless names.include?(name)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
extra_names_to_unlock
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def build_definition(dependencies_to_unlock:)
|
|
110
|
+
gems_to_unlock = dependencies_to_unlock + [dependency_name]
|
|
111
|
+
definition = Bundler::Definition.build(
|
|
112
|
+
gemfile_name,
|
|
113
|
+
lockfile_name,
|
|
114
|
+
gems: gems_to_unlock + subdependencies,
|
|
115
|
+
conservative: true
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Remove the Gemfile / gemspec requirements on the gems we're
|
|
119
|
+
# unlocking (i.e., completely unlock them)
|
|
120
|
+
gems_to_unlock.each do |gem_name|
|
|
121
|
+
unlock_gem(definition: definition, gem_name: gem_name)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
dep = definition.dependencies
|
|
125
|
+
.find { |d| d.name == dependency_name }
|
|
126
|
+
|
|
127
|
+
if dep
|
|
128
|
+
# Set the requirement for the gem we're forcing an update of
|
|
129
|
+
new_req = Gem::Requirement.create("= #{target_version}")
|
|
130
|
+
dep.instance_variable_set(:@requirement, new_req)
|
|
131
|
+
dep.source = nil if dep.source.is_a?(Bundler::Source::Git)
|
|
132
|
+
|
|
133
|
+
definition
|
|
134
|
+
else
|
|
135
|
+
# If the dependency is not found in the Gemfile it means this is a
|
|
136
|
+
# transitive dependency. To force update it, we recreate a definition
|
|
137
|
+
# from the Gemfile, but add an extra dependency to it that pins the
|
|
138
|
+
# dependency we want to update.
|
|
139
|
+
gemfile = Pathname.new(gemfile_name).expand_path
|
|
140
|
+
builder = Bundler::Dsl.new
|
|
141
|
+
builder.eval_gemfile(gemfile)
|
|
142
|
+
builder.gem dependency_name, "= #{target_version}"
|
|
143
|
+
builder.to_definition(
|
|
144
|
+
lockfile_name,
|
|
145
|
+
gems: gems_to_unlock + subdependencies,
|
|
146
|
+
conservative: true
|
|
147
|
+
)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def lockfile
|
|
152
|
+
return @lockfile if defined?(@lockfile)
|
|
153
|
+
|
|
154
|
+
@lockfile =
|
|
155
|
+
begin
|
|
156
|
+
return unless lockfile_name && File.exist?(lockfile_name)
|
|
157
|
+
|
|
158
|
+
File.read(lockfile_name)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def subdependencies
|
|
163
|
+
# If there's no lockfile we don't need to worry about
|
|
164
|
+
# subdependencies
|
|
165
|
+
return [] unless lockfile
|
|
166
|
+
|
|
167
|
+
original_specs.map(&:name) - top_level_dep_names
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def top_level_dep_names
|
|
171
|
+
@top_level_dep_names ||= Bundler::Definition.build(gemfile_name, lockfile_name, {}).dependencies.map(&:name)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def original_specs
|
|
175
|
+
@original_specs ||= Bundler::LockfileParser.new(lockfile).specs
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def unlock_gem(definition:, gem_name:)
|
|
179
|
+
dep = definition.dependencies.find { |d| d.name == gem_name }
|
|
180
|
+
version = definition.locked_gems.specs
|
|
181
|
+
.find { |d| d.name == gem_name }.version
|
|
182
|
+
|
|
183
|
+
dep&.instance_variable_set(
|
|
184
|
+
:@requirement,
|
|
185
|
+
Gem::Requirement.create(">= #{version}")
|
|
186
|
+
)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|