Skip to content
98 changes: 98 additions & 0 deletions updater/lib/dependabot/dependency_change_builder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# frozen_string_literal: true

require "dependabot/dependency"
require "dependabot/dependency_change"
require "dependabot/file_updaters"
require "dependabot/group_rule"

# This class is responsible for generating a DependencyChange for a given
# set of dependencies and dependency files.
#
# This class should be used via the `create_from` method with the following
# arguments:
# - job:
# The Dependabot::Job object the change is originated by
# - dependency_files:
# The list dependency files we aim to modify as part of this change
# - updated_dependencies:
# The set of dependency updates to be applied to the dependency files
# - change_source:
# A change can be generated from either a single 'lead' Dependency or
# a GroupRule
module Dependabot
class DependencyChangeBuilder
def self.create_from(**kwargs)
new(**kwargs).run
end

def initialize(job:, dependency_files:, updated_dependencies:, change_source:)
@job = job
@dependency_files = dependency_files
@updated_dependencies = updated_dependencies
@change_source = change_source
end

def run
updated_files = generate_dependency_files
# Remove any unchanged dependencies from the updated list
updated_deps = updated_dependencies.reject do |d|
# Avoid rejecting the source dependency
next false if source_dependency_name && d.name == source_dependency_name
next true if d.top_level? && d.requirements == d.previous_requirements

d.version == d.previous_version
end

Dependabot::DependencyChange.new(
job: job,
dependencies: updated_deps,
updated_dependency_files: updated_files,
group_rule: source_group_rule
)
end

private

attr_reader :job, :dependency_files, :updated_dependencies, :change_source

def source_dependency_name
return nil unless change_source.is_a? Dependabot::Dependency

change_source.name
end

def source_group_rule
return nil unless change_source.is_a? Dependabot::GroupRule

change_source
end

def generate_dependency_files
if updated_dependencies.count == 1
updated_dependency = updated_dependencies.first
Dependabot.logger.info("Updating #{updated_dependency.name} from " \
"#{updated_dependency.previous_version} to " \
"#{updated_dependency.version}")
else
dependency_names = updated_dependencies.map(&:name)
Dependabot.logger.info("Updating #{dependency_names.join(', ')}")
end

# Ignore dependencies that are tagged as information_only. These will be
# updated indirectly as a result of a parent dependency update and are
# only included here to be included in the PR info.
relevant_dependencies = updated_dependencies.reject(&:informational_only?)
file_updater_for(relevant_dependencies).updated_dependency_files
end

def file_updater_for(dependencies)
Dependabot::FileUpdaters.for_package_manager(job.package_manager).new(
dependencies: dependencies,
dependency_files: dependency_files,
repo_contents_path: job.repo_contents_path,
credentials: job.credentials,
options: job.experiments
)
end
end
end
2 changes: 2 additions & 0 deletions updater/lib/dependabot/job.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require "dependabot/config/ignore_condition"
require "dependabot/config/update_config"
require "dependabot/experiments"
require "dependabot/source"
require "wildcard_matcher"
Expand Down
95 changes: 24 additions & 71 deletions updater/lib/dependabot/updater.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
# frozen_string_literal: true

require "dependabot/config/ignore_condition"
require "dependabot/config/update_config"
require "dependabot/dependency_change"
require "dependabot/dependency_change_builder"
require "dependabot/environment"
require "dependabot/experiments"
require "dependabot/file_fetchers"
require "dependabot/file_updaters"
require "dependabot/logger"
require "dependabot/python"
require "dependabot/terraform"
Expand Down Expand Up @@ -124,7 +122,6 @@ def check_and_update_existing_pr_with_error_handling(dependencies)
end

# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/CyclomaticComplexity
# rubocop:disable Metrics/PerceivedComplexity
# rubocop:disable Metrics/MethodLength
def check_and_update_pull_request(dependencies)
Expand Down Expand Up @@ -180,33 +177,31 @@ def check_and_update_pull_request(dependencies)
requirements_to_unlock: requirements_to_unlock
)

updated_files = generate_dependency_files_for(updated_deps)
updated_deps = updated_deps.reject do |d|
next false if d.name == checker.dependency.name
next true if d.top_level? && d.requirements == d.previous_requirements

d.version == d.previous_version
end
dependency_change = Dependabot::DependencyChangeBuilder.create_from(
job: job,
dependency_files: dependency_snapshot.dependency_files,
updated_dependencies: updated_deps,
change_source: checker.dependency
)

# NOTE: Gradle, Maven and Nuget dependency names can be case-insensitive
# and the dependency name in the security advisory often doesn't match
# what users have specified in their manifest.
job_dependencies = job.dependencies.map(&:downcase)
if updated_deps.map(&:name).map(&:downcase) != job_dependencies
if dependency_change.dependencies.map(&:name).map(&:downcase) != job_dependencies
# The dependencies being updated have changed. Close the existing
# multi-dependency PR and try creating a new one.
close_pull_request(reason: :dependencies_changed)
create_pull_request(updated_deps, updated_files)
elsif existing_pull_request(updated_deps)
create_pull_request(dependency_change)
elsif existing_pull_request(dependency_change.dependencies)
# The existing PR is for this version. Update it.
update_pull_request(updated_deps, updated_files)
update_pull_request(dependency_change)
else
# The existing PR is for a previous version. Supersede it.
create_pull_request(updated_deps, updated_files)
create_pull_request(dependency_change)
end
end
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/CyclomaticComplexity
# rubocop:enable Metrics/PerceivedComplexity
# rubocop:enable Metrics/MethodLength

Expand Down Expand Up @@ -307,14 +302,13 @@ def check_and_create_pull_request(dependency)
)
end

updated_files = generate_dependency_files_for(updated_deps)
updated_deps = updated_deps.reject do |d|
next false if d.name == checker.dependency.name
next true if d.top_level? && d.requirements == d.previous_requirements

d.version == d.previous_version
end
create_pull_request(updated_deps, updated_files)
dependency_change = Dependabot::DependencyChangeBuilder.create_from(
job: job,
dependency_files: dependency_snapshot.dependency_files,
updated_dependencies: updated_deps,
change_source: checker.dependency
)
create_pull_request(dependency_change)
end
# rubocop:enable Metrics/MethodLength
# rubocop:enable Metrics/AbcSize
Expand Down Expand Up @@ -599,48 +593,13 @@ def update_checker_for(dependency, raise_on_ignored:)
)
end

def file_updater_for(dependencies)
Dependabot::FileUpdaters.for_package_manager(job.package_manager).new(
dependencies: dependencies,
dependency_files: dependency_snapshot.dependency_files,
repo_contents_path: job.repo_contents_path,
credentials: job.credentials,
options: job.experiments
)
end

def generate_dependency_files_for(updated_dependencies)
if updated_dependencies.count == 1
updated_dependency = updated_dependencies.first
Dependabot.logger.info("Updating #{updated_dependency.name} from " \
"#{updated_dependency.previous_version} to " \
"#{updated_dependency.version}")
else
dependency_names = updated_dependencies.map(&:name)
Dependabot.logger.info("Updating #{dependency_names.join(', ')}")
end

# Ignore dependencies that are tagged as information_only. These will be
# updated indirectly as a result of a parent dependency update and are
# only included here to be included in the PR info.
deps_to_update = updated_dependencies.reject(&:informational_only?)
updater = file_updater_for(deps_to_update)
updater.updated_dependency_files
end

def create_pull_request(dependencies, updated_dependency_files)
Dependabot.logger.info("Submitting #{dependencies.map(&:name).join(', ')} " \
def create_pull_request(dependency_change)
Dependabot.logger.info("Submitting #{dependency_change.dependencies.map(&:name).join(', ')} " \
"pull request for creation")

dependency_change = Dependabot::DependencyChange.new(
job: job,
dependencies: dependencies,
updated_dependency_files: updated_dependency_files
)

service.create_pull_request(dependency_change, dependency_snapshot.base_commit_sha)

created_pull_requests << dependencies.map do |dep|
created_pull_requests << dependency_change.dependencies.map do |dep|
{
"dependency-name" => dep.name,
"dependency-version" => dep.version,
Expand All @@ -649,16 +608,10 @@ def create_pull_request(dependencies, updated_dependency_files)
end
end

def update_pull_request(dependencies, updated_dependency_files)
Dependabot.logger.info("Submitting #{dependencies.map(&:name).join(', ')} " \
def update_pull_request(dependency_change)
Dependabot.logger.info("Submitting #{dependency_change.dependencies.map(&:name).join(', ')} " \
"pull request for update")

dependency_change = Dependabot::DependencyChange.new(
job: job,
dependencies: dependencies,
updated_dependency_files: updated_dependency_files
)

service.update_pull_request(dependency_change, dependency_snapshot.base_commit_sha)
end

Expand Down
Loading