diff --git a/updater/lib/dependabot/dependency_change_builder.rb b/updater/lib/dependabot/dependency_change_builder.rb new file mode 100644 index 00000000000..e98aea9c619 --- /dev/null +++ b/updater/lib/dependabot/dependency_change_builder.rb @@ -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 diff --git a/updater/lib/dependabot/job.rb b/updater/lib/dependabot/job.rb index 935037b45ce..899e0431007 100644 --- a/updater/lib/dependabot/job.rb +++ b/updater/lib/dependabot/job.rb @@ -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" diff --git a/updater/lib/dependabot/updater.rb b/updater/lib/dependabot/updater.rb index d8ff28a1092..a45a195a863 100644 --- a/updater/lib/dependabot/updater.rb +++ b/updater/lib/dependabot/updater.rb @@ -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" @@ -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) @@ -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 @@ -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 @@ -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, @@ -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 diff --git a/updater/lib/dependabot/updater/operations/group_update_all_versions.rb b/updater/lib/dependabot/updater/operations/group_update_all_versions.rb index 6f94f924a01..aa82d030b0f 100644 --- a/updater/lib/dependabot/updater/operations/group_update_all_versions.rb +++ b/updater/lib/dependabot/updater/operations/group_update_all_versions.rb @@ -32,6 +32,8 @@ def initialize(service:, job:, dependency_snapshot:, error_handler:) @job = job @dependency_snapshot = dependency_snapshot @error_handler = error_handler + # This is a placeholder for a real rule object obtained from config in future + @group_rule = Dependabot::GroupRule.new(name: GROUP_NAME_PLACEHOLDER) end def perform @@ -39,7 +41,7 @@ def perform # We should log the rule being executed, let's just hard-code wildcard for now # since the prototype makes best-effort to do everything in one pass. Dependabot.logger.info("Starting update group for '#{GROUP_NAME_PLACEHOLDER}'") - dependency_change = compile_dependency_change + dependency_change = compile_all_dependency_changes if dependency_change.dependencies.any? Dependabot.logger.info("Creating a pull request for '#{GROUP_NAME_PLACEHOLDER}'") @@ -65,7 +67,8 @@ def perform attr_reader :job, :service, :dependency_snapshot, - :error_handler + :error_handler, + :group_rule def dependencies if dependency_snapshot.dependencies.any? && dependency_snapshot.allowed_dependencies.none? @@ -79,7 +82,7 @@ def dependencies # Returns a Dependabot::DependencyChange object that encapsulates the # outcome of attempting to update every dependency iteratively which # can be used for PR creation. - def compile_dependency_change + def compile_all_dependency_changes all_updated_dependencies = [] updated_files = dependencies.inject(dependency_snapshot.dependency_files) do |dependency_files, dependency| updated_dependencies = compile_updates_for(dependency, dependency_files) @@ -89,7 +92,13 @@ def compile_dependency_change dep.name.casecmp(dependency.name).zero? end - # FIXME: This needs to be de-duped + dependency_change = create_change_for(lead_dependency, updated_dependencies, dependency_files) + + # Move on to the next dependency using the existing files if we + # could not create a change for any reason + next dependency_files unless dependency_change + + # FIXME: all_updated_dependencies may need to be de-duped # # To start out with, using a variant on the 'existing_pull_request' # logic might make sense -or- we could employ a one-and-done rule @@ -99,19 +108,49 @@ def compile_dependency_change # filtering for us assuming we iteratively make file changes for # each Array of dependencies in the batch and the FileUpdater tells # us which cannot be applied. - all_updated_dependencies.concat(updated_dependencies) - generate_dependency_files_for(lead_dependency, updated_dependencies, dependency_files) + all_updated_dependencies.concat(dependency_change.dependencies) + dependency_change.updated_dependency_files else dependency_files # pass on the existing files if no updates are possible end end + # Create a single Dependabot::DependencyChange that aggregates everything we've updated + # into a single object we can pass to PR creation. Dependabot::DependencyChange.new( job: job, dependencies: all_updated_dependencies, updated_dependency_files: updated_files, - group_rule: GROUP_NAME_PLACEHOLDER # This is a placeholder for a real rule object in future + group_rule: group_rule + ) + end + + # This method generates a DependencyChange from the current files and + # list of dependencies to be updated + # + # This method **must** return false in the event of an error + def create_change_for(lead_dependency, updated_dependencies, dependency_files) + Dependabot::DependencyChangeBuilder.create_from( + job: job, + dependency_files: dependency_files, + updated_dependencies: updated_dependencies, + change_source: group_rule + ) + rescue Dependabot::InconsistentRegistryResponse => e + error_handler.log_error( + dependency: lead_dependency, + error: e, + error_type: "inconsistent_registry_response", + error_detail: e.message ) + + false + rescue StandardError => e + raise if ErrorHandler::RUN_HALTING_ERRORS.keys.any? { |err| e.is_a?(err) } + + error_handler.handle_dependabot_error(error: e, dependency: lead_dependency) + + false end # This method determines which dependencies must change given a target @@ -155,7 +194,7 @@ def compile_updates_for(dependency, dependency_files) return [] end - filter_unrelated_and_unchanged(updated_deps, checker) + updated_deps rescue Dependabot::InconsistentRegistryResponse => e error_handler.log_error( dependency: dependency, @@ -169,15 +208,6 @@ def compile_updates_for(dependency, dependency_files) [] # return an empty set end - def filter_unrelated_and_unchanged(updated_dependencies, checker) - updated_dependencies.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 - end - def log_up_to_date(dependency) Dependabot.logger.info( "No update needed for #{dependency.name} #{dependency.version}" @@ -202,16 +232,6 @@ def update_checker_for(dependency, dependency_files, raise_on_ignored:) ) end - def file_updater_for(dependencies, dependency_files) - 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 - def log_checking_for_update(dependency) Dependabot.logger.info( "Checking if #{dependency.name} #{dependency.version} needs updating" @@ -267,49 +287,6 @@ def peer_dependency_should_update_instead?(dependency_name, updated_deps) can_update?(requirements_to_unlock: :own) end end - - # This method generates new dependency files from the current files and list of dependencies to - # be updated - # - # This method **must** return the current files in the event of an error - def generate_dependency_files_for(lead_dependency, updated_dependencies, current_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. - deps_to_update = updated_dependencies.reject(&:informational_only?) - updater = file_updater_for(deps_to_update, current_dependency_files) - updated_files = updater.updated_dependency_files - # If we couldn't update anything, sent back the original files - updated_files.any? ? updated_files : current_dependency_files - # FIXME: Can the updated files include a subset of the input files? - # - # We should unit test and establish a tolerance for this behaviour - # to avoid the downstream contract changing regardless of what the - # real-world behaviour is. - rescue Dependabot::InconsistentRegistryResponse => e - error_handler.log_error( - dependency: lead_dependency, - error: e, - error_type: "inconsistent_registry_response", - error_detail: e.message - ) - current_dependency_files # return the files unchanged - rescue StandardError => e - raise if ErrorHandler::RUN_HALTING_ERRORS.keys.any? { |err| e.is_a?(err) } - - error_handler.handle_dependabot_error(error: e, dependency: lead_dependency) - current_dependency_files # return the files unchanged - end end end end diff --git a/updater/lib/dependabot/updater/operations/refresh_version_update_pull_request.rb b/updater/lib/dependabot/updater/operations/refresh_version_update_pull_request.rb index beda55a4aaf..88e997adfef 100644 --- a/updater/lib/dependabot/updater/operations/refresh_version_update_pull_request.rb +++ b/updater/lib/dependabot/updater/operations/refresh_version_update_pull_request.rb @@ -86,57 +86,44 @@ 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/PerceivedComplexity - 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) 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 @@ -165,16 +152,6 @@ 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 log_checking_for_update(dependency) Dependabot.logger.info( "Checking if #{dependency.name} #{dependency.version} needs updating" @@ -226,25 +203,6 @@ def existing_pull_request(updated_dependencies) job.existing_pull_requests.find { |pr| Set.new(pr) == new_pr_set } 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 end end end diff --git a/updater/lib/dependabot/updater/operations/update_all_versions.rb b/updater/lib/dependabot/updater/operations/update_all_versions.rb index 87248dd4d33..649c615f3c1 100644 --- a/updater/lib/dependabot/updater/operations/update_all_versions.rb +++ b/updater/lib/dependabot/updater/operations/update_all_versions.rb @@ -115,22 +115,17 @@ def check_and_create_pull_request(dependency) ) end - updated_files = generate_dependency_files_for(updated_deps) - updated_deps = filter_unrelated_and_unchanged(updated_deps, checker) - 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 - def filter_unrelated_and_unchanged(updated_dependencies, checker) - updated_dependencies.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 - end - def log_up_to_date(dependency) Dependabot.logger.info( "No update needed for #{dependency.name} #{dependency.version}" @@ -155,16 +150,6 @@ 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 log_checking_for_update(dependency) Dependabot.logger.info( "Checking if #{dependency.name} #{dependency.version} needs updating" @@ -248,38 +233,13 @@ def peer_dependency_should_update_instead?(dependency_name, updated_deps) end 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, diff --git a/updater/spec/dependabot/dependency_change_builder_spec.rb b/updater/spec/dependabot/dependency_change_builder_spec.rb new file mode 100644 index 00000000000..eb6b59b78e3 --- /dev/null +++ b/updater/spec/dependabot/dependency_change_builder_spec.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true + +require "spec_helper" +require "dependabot/dependency_change_builder" +require "dependabot/job" + +RSpec.describe Dependabot::DependencyChangeBuilder do + let(:job) do + instance_double(Dependabot::Job, + package_manager: "bundler", + repo_contents_path: nil, + credentials: [ + { + "type" => "git_source", + "host" => "github.com", + "username" => "x-access-token", + "password" => "github-token" + } + ], + experiments: {}) + end + + let(:dependency_files) do + [ + Dependabot::DependencyFile.new( + name: "Gemfile", + content: fixture("bundler/original/Gemfile"), + directory: "/" + ), + Dependabot::DependencyFile.new( + name: "Gemfile.lock", + content: fixture("bundler/original/Gemfile.lock"), + directory: "/" + ) + ] + end + + let(:updated_dependencies) do + [ + Dependabot::Dependency.new( + name: "dummy-pkg-b", + package_manager: "bundler", + version: "1.2.0", + previous_version: "1.1.0", + requirements: [ + { + file: "Gemfile", + requirement: "~> 1.2.0", + groups: [], + source: nil + } + ], + previous_requirements: [ + { + file: "Gemfile", + requirement: "~> 1.1.0", + groups: [], + source: nil + } + ] + ) + ] + end + + describe "::create_from" do + subject(:create_change) do + described_class.create_from( + job: job, + dependency_files: dependency_files, + updated_dependencies: updated_dependencies, + change_source: change_source + ) + end + + context "when the source is a lead dependency" do + let(:change_source) do + Dependabot::Dependency.new( + name: "dummy-pkg-b", + package_manager: "bundler", + version: "1.1.0", + requirements: [ + { + file: "Gemfile", + requirement: "~> 1.1.0", + groups: [], + source: nil + } + ] + ) + end + + it "creates a new DependencyChange with the updated files" do + dependency_change = create_change + + expect(dependency_change).to be_a(Dependabot::DependencyChange) + expect(dependency_change.dependencies).to eql(updated_dependencies) + expect(dependency_change.updated_dependency_files.map(&:name)).to eql(["Gemfile", "Gemfile.lock"]) + expect(dependency_change).not_to be_grouped_update + + gemfile = dependency_change.updated_dependency_files.find { |file| file.name == "Gemfile" } + expect(gemfile.content).to eql(fixture("bundler/updated/Gemfile")) + + lockfile = dependency_change.updated_dependency_files.find { |file| file.name == "Gemfile.lock" } + expect(lockfile.content).to eql(fixture("bundler/updated/Gemfile.lock")) + end + end + + context "when the source is a group rule" do + let(:change_source) do + Dependabot::GroupRule.new(name: "dummy-pkg-*") + end + + it "creates a new DependencyChange flagged as a grouped update" do + dependency_change = create_change + + expect(dependency_change).to be_a(Dependabot::DependencyChange) + expect(dependency_change).to be_grouped_update + end + end + end +end diff --git a/updater/spec/dependabot/updater_spec.rb b/updater/spec/dependabot/updater_spec.rb index 2a6b463abd9..b769defe1d3 100644 --- a/updater/spec/dependabot/updater_spec.rb +++ b/updater/spec/dependabot/updater_spec.rb @@ -410,7 +410,7 @@ updater = build_updater(service: service, job: job) expect(checker).to receive(:up_to_date?).and_return(true) - expect(updater).to_not receive(:generate_dependency_files_for) + expect(Dependabot::DependencyChangeBuilder).to_not receive(:create_from) expect(service).to_not receive(:create_pull_request) expect(service).to receive(:record_update_job_error). with( @@ -789,7 +789,7 @@ def expect_update_checker_with_ignored_versions(versions) service = build_service updater = build_updater(service: service, job: job) - expect(updater).to_not receive(:generate_dependency_files_for) + expect(Dependabot::DependencyChangeBuilder).to_not receive(:create_from) expect(service).to_not receive(:create_pull_request) updater.run @@ -895,7 +895,7 @@ def expect_update_checker_with_ignored_versions(versions) updater = build_updater(service: service, job: job) expect(checker).to_not receive(:can_update?) - expect(updater).to_not receive(:generate_dependency_files_for) + expect(Dependabot::DependencyChangeBuilder).to_not receive(:create_from) expect(service).to_not receive(:create_pull_request) expect(service).to_not receive(:record_update_job_error) expect(Dependabot.logger). @@ -924,7 +924,7 @@ def expect_update_checker_with_ignored_versions(versions) expect(checker).to receive(:up_to_date?).and_return(false, false) expect(checker).to receive(:can_update?).and_return(true, false) - expect(updater).to_not receive(:generate_dependency_files_for) + expect(Dependabot::DependencyChangeBuilder).to_not receive(:create_from) expect(service).to_not receive(:create_pull_request) expect(service).to_not receive(:record_update_job_error) expect(Dependabot.logger). @@ -961,7 +961,7 @@ def expect_update_checker_with_ignored_versions(versions) expect(checker).to receive(:up_to_date?).and_return(false) expect(checker).to receive(:can_update?).and_return(true) - expect(updater).to_not receive(:generate_dependency_files_for) + expect(Dependabot::DependencyChangeBuilder).to_not receive(:create_from) expect(service).to_not receive(:create_pull_request) expect(service).to receive(:record_update_job_error). with( @@ -1006,7 +1006,7 @@ def expect_update_checker_with_ignored_versions(versions) updater = build_updater(service: service, job: job) expect(checker).to_not receive(:can_update?) - expect(updater).to_not receive(:generate_dependency_files_for) + expect(Dependabot::DependencyChangeBuilder).to_not receive(:create_from) expect(service).to_not receive(:create_pull_request) expect(service).to receive(:record_update_job_error). with( @@ -1105,7 +1105,7 @@ def expect_update_checker_with_ignored_versions(versions) expect(checker).to receive(:up_to_date?).and_return(false) expect(checker).to receive(:can_update?).and_return(true) - expect(updater).to_not receive(:generate_dependency_files_for) + expect(Dependabot::DependencyChangeBuilder).to_not receive(:create_from) expect(service).to_not receive(:create_pull_request) expect(service).to receive(:record_update_job_error). with(