Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Explain update not possible for bundler #2705

Merged
merged 13 commits into from
Nov 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions bin/dry-run.rb
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@
commit: nil,
updater_options: {},
security_advisories: [],
security_updates_only: false,
security_updates_only: false
}

unless ENV["LOCAL_GITHUB_ACCESS_TOKEN"].to_s.strip.empty?
Expand Down Expand Up @@ -424,7 +424,7 @@ def file_updater_for(dependencies)
dependency_files: $files,
repo_contents_path: $repo_contents_path,
credentials: $options[:credentials],
options: $options[:updater_options],
options: $options[:updater_options]
)
end

Expand Down Expand Up @@ -482,11 +482,25 @@ def security_fix?(dependency)
end
end

latest_allowed_version = checker.vulnerable? ?
checker.lowest_resolvable_security_fix_version :
checker.latest_resolvable_version
latest_allowed_version = if checker.vulnerable?
checker.lowest_resolvable_security_fix_version
else
checker.latest_resolvable_version
end
puts " => latest allowed version is #{latest_allowed_version || dep.version}"

conflicting_dependencies = checker.conflicting_dependencies
if conflicting_dependencies.any?
puts " => The update is not possible because of the following conflicting "\
"dependencies:"

conflicting_dependencies.each do |conflicting_dep|
puts " #{conflicting_dep['name']} (#{conflicting_dep['version']}) "\
"which requires:"
puts " #{dep.name} #{conflicting_dep['requirement']}"
end
end

if checker.up_to_date?
puts " (no update needed as it's already up-to-date)"
next
Expand Down
30 changes: 21 additions & 9 deletions bundler/helpers/lib/functions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require "functions/lockfile_updater"
require "functions/dependency_source"
require "functions/version_resolver"
require "functions/conflicting_dependency_resolver"

module Functions
def self.parsed_gemfile(lockfile_name:, gemfile_name:, dir:)
Expand Down Expand Up @@ -32,7 +33,7 @@ def self.update_lockfile(dir:, gemfile_name:, lockfile_name:, using_bundler_2:,
LockfileUpdater.new(
gemfile_name: gemfile_name,
lockfile_name: lockfile_name,
dependencies: dependencies,
dependencies: dependencies
).run
end

Expand All @@ -46,7 +47,7 @@ def self.force_update(dir:, dependency_name:, target_version:, gemfile_name:,
target_version: target_version,
gemfile_name: gemfile_name,
lockfile_name: lockfile_name,
update_multiple_dependencies: update_multiple_dependencies,
update_multiple_dependencies: update_multiple_dependencies
).run
end

Expand All @@ -57,7 +58,7 @@ def self.dependency_source_type(gemfile_name:, dependency_name:, dir:,

DependencySource.new(
gemfile_name: gemfile_name,
dependency_name: dependency_name,
dependency_name: dependency_name
).type
end

Expand All @@ -69,7 +70,7 @@ def self.depencency_source_latest_git_version(gemfile_name:, dependency_name:,
using_bundler_2: false)
DependencySource.new(
gemfile_name: gemfile_name,
dependency_name: dependency_name,
dependency_name: dependency_name
).latest_git_version(
dependency_source_url: dependency_source_url,
dependency_source_branch: dependency_source_branch
Expand All @@ -83,7 +84,7 @@ def self.private_registry_versions(gemfile_name:, dependency_name:, dir:,

DependencySource.new(
gemfile_name: gemfile_name,
dependency_name: dependency_name,
dependency_name: dependency_name
).private_registry_versions
end

Expand All @@ -96,7 +97,7 @@ def self.resolve_version(dependency_name:, dependency_requirements:,
dependency_name: dependency_name,
dependency_requirements: dependency_requirements,
gemfile_name: gemfile_name,
lockfile_name: lockfile_name,
lockfile_name: lockfile_name
).version_details
end

Expand All @@ -117,9 +118,9 @@ def self.git_specs(dir:, gemfile_name:, credentials:, using_bundler_2:)
using_bundler_2: using_bundler_2)

git_specs = Bundler::Definition.build(gemfile_name, nil, {}).dependencies.
select do |spec|
spec.source.is_a?(Bundler::Source::Git)
end
select do |spec|
spec.source.is_a?(Bundler::Source::Git)
end
git_specs.map do |spec|
# Piggy-back off some private Bundler methods to configure the
# URI with auth details in the same way Bundler does.
Expand Down Expand Up @@ -161,4 +162,15 @@ def self.set_bundler_flags_and_credentials(dir:, credentials:,
Bundler.settings.set_command_option("github.https", "true")
end
end

def self.conflicting_dependencies(dir:, dependency_name:, target_version:,
lockfile_name:, using_bundler_2:, credentials:)
set_bundler_flags_and_credentials(dir: dir, credentials: credentials,
using_bundler_2: using_bundler_2)
ConflictingDependencyResolver.new(
dependency_name: dependency_name,
target_version: target_version,
lockfile_name: lockfile_name
).conflicting_dependencies
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# frozen_string_literal: true

module Functions
class ConflictingDependencyResolver
def initialize(dependency_name:, target_version:, lockfile_name:)
@dependency_name = dependency_name
@target_version = target_version
@lockfile_name = lockfile_name
end

# Finds any dependencies in the lockfile that have a subdependency on the
# given dependency that does not satisfly the target_version.
# @return [Array<Hash{String => String}]
# * name [String] the blocking dependencies name
# * version [String] the version of the blocking dependency
# * requirement [String] the requirement on the target_dependency
def conflicting_dependencies
Bundler.settings.set_command_option("only_update_to_newer_versions", true)

parent_specs.map do |spec|
req = spec.dependencies.find { |bd| bd.name == dependency_name }
{
"name" => spec.name,
"version" => spec.version.to_s,
"requirement" => req.requirement.to_s
}
end
end

private

attr_reader :dependency_name, :target_version, :lockfile_name

def parent_specs
version = Gem::Version.new(target_version)
Bundler::LockfileParser.new(lockfile).specs.filter do |spec|
spec.dependencies.any? do |sub_dep|
sub_dep.name == dependency_name &&
!sub_dep.requirement.satisfied_by?(version)
end
end
end

def lockfile
return @lockfile if defined?(@lockfile)

@lockfile =
begin
return unless lockfile_name && File.exist?(lockfile_name)

File.read(lockfile_name)
end
end
end
end
23 changes: 14 additions & 9 deletions bundler/helpers/lib/functions/force_updater.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
module Functions
class ForceUpdater
class TransitiveDependencyError < StandardError; end
jurre marked this conversation as resolved.
Show resolved Hide resolved

def initialize(dependency_name:, target_version:, gemfile_name:,
lockfile_name:, update_multiple_dependencies:)
@dependency_name = dependency_name
Expand All @@ -24,11 +26,11 @@ def run
definition.resolve_remotely!
specs = definition.resolve
updates = [{ name: dependency_name }] +
other_updates.map { |dep| { name: dep.name } }
other_updates.map { |dep| { name: dep.name } }
specs = specs.map do |dep|
{
name: dep.name,
version: dep.version,
version: dep.version
}
end
[updates, specs]
Expand All @@ -54,7 +56,7 @@ def run
attr_reader :dependency_name, :target_version, :gemfile_name,
:lockfile_name, :credentials,
:update_multiple_dependencies
alias :update_multiple_dependencies? :update_multiple_dependencies
alias update_multiple_dependencies? update_multiple_dependencies

def new_dependencies_to_unlock_from(error:, already_unlocked:)
potentials_deps =
Expand Down Expand Up @@ -111,14 +113,17 @@ def build_definition(other_updates:)
unlock_gem(definition: definition, gem_name: gem_name)
end

dep = definition.dependencies.
find { |d| d.name == dependency_name }

# If the dependency is not found in the Gemfile it means this is a
# transitive dependency that we can't force update.
raise TransitiveDependencyError unless dep

# Set the requirement for the gem we're forcing an update of
new_req = Gem::Requirement.create("= #{target_version}")
definition.dependencies.
find { |d| d.name == dependency_name }.
tap do |dep|
dep.instance_variable_set(:@requirement, new_req)
dep.source = nil if dep.source.is_a?(Bundler::Source::Git)
end
dep.instance_variable_set(:@requirement, new_req)
dep.source = nil if dep.source.is_a?(Bundler::Source::Git)

definition
end
Expand Down
12 changes: 12 additions & 0 deletions bundler/lib/dependabot/bundler/update_checker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class UpdateChecker < Dependabot::UpdateCheckers::Base
require_relative "update_checker/requirements_updater"
require_relative "update_checker/version_resolver"
require_relative "update_checker/latest_version_finder"
require_relative "update_checker/conflicting_dependency_resolver"

def latest_version
return latest_version_for_git_dependency if git_dependency?
Expand Down Expand Up @@ -107,6 +108,17 @@ def requirements_update_strategy
dependency.version.nil? ? :bump_versions_if_necessary : :bump_versions
end

def conflicting_dependencies
ConflictingDependencyResolver.new(
dependency_files: dependency_files,
repo_contents_path: repo_contents_path,
credentials: credentials
).conflicting_dependencies(
dependency: dependency,
target_version: lowest_security_fix_version
)
end

private

def latest_version_resolvable_with_full_unlock?
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# frozen_string_literal: true

require "dependabot/bundler/update_checker"
require "dependabot/bundler/native_helpers"
require "dependabot/shared_helpers"

module Dependabot
module Bundler
class UpdateChecker < UpdateCheckers::Base
class ConflictingDependencyResolver
require_relative "shared_bundler_helpers"
include SharedBundlerHelpers

def initialize(dependency_files:, repo_contents_path:, credentials:)
@dependency_files = dependency_files
@repo_contents_path = repo_contents_path
@credentials = credentials
end

# Finds any dependencies in the lockfile that have a subdependency on
# the given dependency that does not satisfly the target_version.
#
# @param dependency [Dependabot::Dependency] the dependency to check
# @param target_version [String] the version to check
# @return [Array<Hash{String => String}]
# * name [String] the blocking dependencies name
# * version [String] the version of the blocking dependency
# * requirement [String] the requirement on the target_dependency
def conflicting_dependencies(dependency:, target_version:)
in_a_native_bundler_context(error_handling: false) do |tmp_dir|
SharedHelpers.run_helper_subprocess(
command: NativeHelpers.helper_path,
function: "conflicting_dependencies",
args: {
dir: tmp_dir,
dependency_name: dependency.name,
target_version: target_version,
credentials: relevant_credentials,
lockfile_name: lockfile.name,
using_bundler_2: using_bundler_2?
}
)
end
end
end
end
end
end
Loading