class Dependabot::PullRequestCreator::MessageBuilder

MessageBuilder builds PR message for a dependency update

Attributes

commit_message_options[R]
credentials[R]
dependencies[R]
files[R]
github_redirection_service[R]
pr_message_header[R]
source[R]
vulnerabilities_fixed[R]

Public Class Methods

new(source:, dependencies:, files:, credentials:, pr_message_header: nil, pr_message_footer: nil, commit_message_options: {}, vulnerabilities_fixed: {}, github_redirection_service:) click to toggle source
# File lib/dependabot/pull_request_creator/message_builder.rb, line 25
def initialize(source:, dependencies:, files:, credentials:,
               pr_message_header: nil, pr_message_footer: nil,
               commit_message_options: {}, vulnerabilities_fixed: {},
               github_redirection_service:)
  @dependencies               = dependencies
  @files                      = files
  @source                     = source
  @credentials                = credentials
  @pr_message_header          = pr_message_header
  @pr_message_footer          = pr_message_footer
  @commit_message_options     = commit_message_options
  @vulnerabilities_fixed      = vulnerabilities_fixed
  @github_redirection_service = github_redirection_service
end

Public Instance Methods

commit_message() click to toggle source
# File lib/dependabot/pull_request_creator/message_builder.rb, line 53
def commit_message
  message = commit_subject + "\n\n"
  message += commit_message_intro
  message += metadata_links
  message += "\n\n" + message_trailers if message_trailers
  message
end
message() click to toggle source
# File lib/dependabot/pull_request_creator/message_builder.rb, line 61
def message
  Dependabot::PullRequestCreator::Message.new(
    pr_name: pr_name,
    pr_message: pr_message,
    commit_message: commit_message
  )
end
pr_message() click to toggle source
# File lib/dependabot/pull_request_creator/message_builder.rb, line 48
def pr_message
  suffixed_pr_message_header + commit_message_intro + \
    metadata_cascades + prefixed_pr_message_footer
end
pr_name() click to toggle source
# File lib/dependabot/pull_request_creator/message_builder.rb, line 40
def pr_name
  pr_name = pr_name_prefixer.pr_name_prefix
  pr_name += library? ? library_pr_name : application_pr_name
  return pr_name if files.first.directory == "/"

  pr_name + " in #{files.first.directory}"
end

Private Instance Methods

application_pr_name() click to toggle source
# File lib/dependabot/pull_request_creator/message_builder.rb, line 86
def application_pr_name
  pr_name = "bump "
  pr_name = pr_name.capitalize if pr_name_prefixer.capitalize_first_word?

  pr_name +
    if dependencies.count == 1
      dependency = dependencies.first
      "#{dependency.display_name} "\
      "#{from_version_msg(previous_version(dependency))}"\
      "to #{new_version(dependency)}"
    elsif updating_a_property?
      dependency = dependencies.first
      "#{property_name} "\
      "#{from_version_msg(previous_version(dependency))}"\
      "to #{new_version(dependency)}"
    elsif updating_a_dependency_set?
      dependency = dependencies.first
      "#{dependency_set.fetch(:group)} dependency set "\
      "#{from_version_msg(previous_version(dependency))}"\
      "to #{new_version(dependency)}"
    else
      names = dependencies.map(&:name)
      "#{names[0..-2].join(', ')} and #{names[-1]}"
    end
end
changelog_url(dependency) click to toggle source
# File lib/dependabot/pull_request_creator/message_builder.rb, line 326
def changelog_url(dependency)
  metadata_finder(dependency).changelog_url
end
commit_message_intro() click to toggle source
# File lib/dependabot/pull_request_creator/message_builder.rb, line 126
def commit_message_intro
  return requirement_commit_message_intro if library?

  version_commit_message_intro
end
commit_subject() click to toggle source
# File lib/dependabot/pull_request_creator/message_builder.rb, line 116
def commit_subject
  subject = pr_name.gsub("⬆️", ":arrow_up:").gsub("🔒", ":lock:")
  return subject unless subject.length > 72

  subject = subject.gsub(/ from [^\s]*? to [^\s]*/, "")
  return subject unless subject.length > 72

  subject.split(" in ").first
end
commits_url(dependency) click to toggle source
# File lib/dependabot/pull_request_creator/message_builder.rb, line 330
def commits_url(dependency)
  metadata_finder(dependency).commits_url
end
dependency_set() click to toggle source
# File lib/dependabot/pull_request_creator/message_builder.rb, line 256
def dependency_set
  @dependency_set ||= dependencies.first.requirements.
                      find { |r| r.dig(:metadata, :dependency_set) }&.
                      dig(:metadata, :dependency_set)

  raise "No dependency set!" unless @dependency_set

  @dependency_set
end
dependency_set_intro() click to toggle source
# File lib/dependabot/pull_request_creator/message_builder.rb, line 214
def dependency_set_intro
  dependency = dependencies.first

  "Bumps `#{dependency_set.fetch(:group)}` "\
  "dependency set #{from_version_msg(previous_version(dependency))}"\
  "to #{new_version(dependency)}."
end
docker_digest_from_reqs(requirements) click to toggle source
# File lib/dependabot/pull_request_creator/message_builder.rb, line 404
def docker_digest_from_reqs(requirements)
  requirements.
    map { |r| r.dig(:source, "digest") || r.dig(:source, :digest) }.
    compact.first
end
from_version_msg(previous_version) click to toggle source
# File lib/dependabot/pull_request_creator/message_builder.rb, line 228
def from_version_msg(previous_version)
  return "" unless previous_version

  "from #{previous_version} "
end
homepage_url(dependency) click to toggle source
# File lib/dependabot/pull_request_creator/message_builder.rb, line 334
def homepage_url(dependency)
  metadata_finder(dependency).homepage_url
end
library?() click to toggle source

TODO: Bring this in line with existing library checks that we do in the update checkers, which are also overriden by passing an explicit `requirements_update_strategy`.

TODO re-use in BranchNamer

# File lib/dependabot/pull_request_creator/message_builder.rb, line 461
def library?
  # Reject any nested child gemspecs/vendored git dependencies
  root_files = files.map(&:name).
               select { |p| Pathname.new(p).dirname.to_s == "." }
  return true if root_files.select { |nm| nm.end_with?(".gemspec") }.any?

  dependencies.any? { |d| previous_version(d).nil? }
end
library_pr_name() click to toggle source
# File lib/dependabot/pull_request_creator/message_builder.rb, line 71
def library_pr_name
  pr_name = "update "
  pr_name = pr_name.capitalize if pr_name_prefixer.capitalize_first_word?

  pr_name +
    if dependencies.count == 1
      "#{dependencies.first.display_name} requirement "\
      "#{from_version_msg(old_library_requirement(dependencies.first))}"\
      "to #{new_library_requirement(dependencies.first)}"
    else
      names = dependencies.map(&:name)
      "requirements for #{names[0..-2].join(', ')} and #{names[-1]}"
    end
end
message_trailers() click to toggle source
# File lib/dependabot/pull_request_creator/message_builder.rb, line 144
def message_trailers
  return unless on_behalf_of_message || signoff_message

  [on_behalf_of_message, signoff_message].compact.join("\n")
end
metadata_cascades() click to toggle source
# File lib/dependabot/pull_request_creator/message_builder.rb, line 298
def metadata_cascades
  return metadata_cascades_for_dep(dependencies.first) if dependencies.one?

  dependencies.map do |dep|
    msg = "\nUpdates `#{dep.display_name}` "\
          "#{from_version_msg(previous_version(dep))}"\
          "to #{new_version(dep)}"

    if vulnerabilities_fixed[dep.name]&.one?
      msg += " **This update includes a security fix.**"
    elsif vulnerabilities_fixed[dep.name]&.any?
      msg += " **This update includes security fixes.**"
    end

    msg + metadata_cascades_for_dep(dep)
  end.join
end
metadata_cascades_for_dep(dependency) click to toggle source
# File lib/dependabot/pull_request_creator/message_builder.rb, line 316
def metadata_cascades_for_dep(dependency)
  MetadataPresenter.new(
    dependency: dependency,
    source: source,
    metadata_finder: metadata_finder(dependency),
    vulnerabilities_fixed: vulnerabilities_fixed[dependency.name],
    github_redirection_service: github_redirection_service
  ).to_s
end
metadata_finder(dependency) click to toggle source
# File lib/dependabot/pull_request_creator/message_builder.rb, line 350
def metadata_finder(dependency)
  @metadata_finder ||= {}
  @metadata_finder[dependency.name] ||=
    MetadataFinders.
    for_package_manager(dependency.package_manager).
    new(dependency: dependency, credentials: credentials)
end
multidependency_intro() click to toggle source
# File lib/dependabot/pull_request_creator/message_builder.rb, line 222
def multidependency_intro
  "Bumps #{dependency_links[0..-2].join(', ')} "\
  "and #{dependency_links[-1]}. These "\
  "dependencies needed to be updated together."
end
multidependency_property_intro() click to toggle source

rubocop:enable Metrics/PerceivedComplexity

# File lib/dependabot/pull_request_creator/message_builder.rb, line 206
def multidependency_property_intro
  dependency = dependencies.first

  "Bumps `#{property_name}` "\
  "#{from_version_msg(previous_version(dependency))}"\
  "to #{new_version(dependency)}."
end
new_library_requirement(dependency) click to toggle source
# File lib/dependabot/pull_request_creator/message_builder.rb, line 437
def new_library_requirement(dependency)
  updated_reqs =
    dependency.requirements - dependency.previous_requirements

  gemspec =
    updated_reqs.find { |r| r[:file].match?(%r{^[^/]*\.gemspec$}) }
  return gemspec.fetch(:requirement) if gemspec

  req = updated_reqs.first.fetch(:requirement)
  return req if req
  return new_ref(dependency) if ref_changed?(dependency) && new_ref(dependency)

  raise "No new requirement!"
end
new_ref(dependency) click to toggle source
# File lib/dependabot/pull_request_creator/message_builder.rb, line 417
def new_ref(dependency)
  new_refs = dependency.requirements.map do |r|
    r.dig(:source, "ref") || r.dig(:source, :ref)
  end.compact.uniq
  return new_refs.first if new_refs.count == 1
end
new_version(dependency) click to toggle source
# File lib/dependabot/pull_request_creator/message_builder.rb, line 390
def new_version(dependency)
  if dependency.version.match?(/^[0-9a-f]{40}$/)
    return new_ref(dependency) if ref_changed?(dependency) && new_ref(dependency)

    "`#{dependency.version[0..6]}`"
  elsif dependency.version == dependency.previous_version &&
        package_manager == "docker"
    digest = docker_digest_from_reqs(dependency.requirements)
    "`#{digest.split(':').last[0..6]}`"
  else
    dependency.version
  end
end
old_library_requirement(dependency) click to toggle source
# File lib/dependabot/pull_request_creator/message_builder.rb, line 424
def old_library_requirement(dependency)
  old_reqs =
    dependency.previous_requirements - dependency.requirements

  gemspec =
    old_reqs.find { |r| r[:file].match?(%r{^[^/]*\.gemspec$}) }
  return gemspec.fetch(:requirement) if gemspec

  req = old_reqs.first.fetch(:requirement)
  return req if req
  return previous_ref(dependency) if ref_changed?(dependency)
end
on_behalf_of_message() click to toggle source
# File lib/dependabot/pull_request_creator/message_builder.rb, line 158
def on_behalf_of_message
  signoff_details = commit_message_options[:signoff_details]
  return unless signoff_details.is_a?(Hash)
  return unless signoff_details[:org_name] && signoff_details[:org_email]

  "On-behalf-of: @#{signoff_details[:org_name]} "\
  "<#{signoff_details[:org_email]}>"
end
package_manager() click to toggle source
# File lib/dependabot/pull_request_creator/message_builder.rb, line 479
def package_manager
  @package_manager ||= dependencies.first.package_manager
end
pr_name_prefix() click to toggle source
# File lib/dependabot/pull_request_creator/message_builder.rb, line 112
def pr_name_prefix
  pr_name_prefixer.pr_name_prefix
end
pr_name_prefixer() click to toggle source
# File lib/dependabot/pull_request_creator/message_builder.rb, line 358
def pr_name_prefixer
  @pr_name_prefixer ||=
    PrNamePrefixer.new(
      source: source,
      dependencies: dependencies,
      credentials: credentials,
      commit_message_options: commit_message_options,
      security_fix: vulnerabilities_fixed.values.flatten.any?
    )
end
previous_ref(dependency) click to toggle source
# File lib/dependabot/pull_request_creator/message_builder.rb, line 410
def previous_ref(dependency)
  previous_refs = dependency.previous_requirements.map do |r|
    r.dig(:source, "ref") || r.dig(:source, :ref)
  end.compact.uniq
  return previous_refs.first if previous_refs.count == 1
end
previous_version(dependency) click to toggle source
# File lib/dependabot/pull_request_creator/message_builder.rb, line 369
def previous_version(dependency)
  # If we don't have a previous version, we *may* still be able to figure
  # one out if a ref was provided and has been changed (in which case the
  # previous ref was essentially the version).
  if dependency.previous_version.nil?
    return ref_changed?(dependency) ? previous_ref(dependency) : nil
  end

  if dependency.previous_version.match?(/^[0-9a-f]{40}$/)
    return previous_ref(dependency) if ref_changed?(dependency) && previous_ref(dependency)

    "`#{dependency.previous_version[0..6]}`"
  elsif dependency.version == dependency.previous_version &&
        package_manager == "docker"
    digest = docker_digest_from_reqs(dependency.previous_requirements)
    "`#{digest.split(':').last[0..6]}`"
  else
    dependency.previous_version
  end
end
property_name() click to toggle source
# File lib/dependabot/pull_request_creator/message_builder.rb, line 246
def property_name
  @property_name ||= dependencies.first.requirements.
                     find { |r| r.dig(:metadata, :property_name) }&.
                     dig(:metadata, :property_name)

  raise "No property name!" unless @property_name

  @property_name
end
ref_changed?(dependency) click to toggle source
# File lib/dependabot/pull_request_creator/message_builder.rb, line 452
def ref_changed?(dependency)
  previous_ref(dependency) != new_ref(dependency)
end
releases_url(dependency) click to toggle source
# File lib/dependabot/pull_request_creator/message_builder.rb, line 338
def releases_url(dependency)
  metadata_finder(dependency).releases_url
end
requirement_commit_message_intro() click to toggle source
# File lib/dependabot/pull_request_creator/message_builder.rb, line 167
def requirement_commit_message_intro
  msg = "Updates the requirements on "

  msg +=
    if dependencies.count == 1
      "#{dependency_links.first} "
    else
      "#{dependency_links[0..-2].join(', ')} and #{dependency_links[-1]} "
    end

  msg + "to permit the latest version."
end
signoff_message() click to toggle source
# File lib/dependabot/pull_request_creator/message_builder.rb, line 150
def signoff_message
  signoff_details = commit_message_options[:signoff_details]
  return unless signoff_details.is_a?(Hash)
  return unless signoff_details[:name] && signoff_details[:email]

  "Signed-off-by: #{signoff_details[:name]} <#{signoff_details[:email]}>"
end
source_url(dependency) click to toggle source
# File lib/dependabot/pull_request_creator/message_builder.rb, line 342
def source_url(dependency)
  metadata_finder(dependency).source_url
end
suffixed_pr_message_header() click to toggle source
# File lib/dependabot/pull_request_creator/message_builder.rb, line 138
def suffixed_pr_message_header
  return "" unless pr_message_header

  "#{pr_message_header}\n\n"
end
switching_from_ref_to_release?(dependency) click to toggle source
# File lib/dependabot/pull_request_creator/message_builder.rb, line 470
def switching_from_ref_to_release?(dependency)
  unless dependency.previous_version&.match?(/^[0-9a-f]{40}$/) ||
         dependency.previous_version.nil? && previous_ref(dependency)
    return false
  end

  Gem::Version.correct?(dependency.version)
end
updating_a_dependency_set?() click to toggle source
# File lib/dependabot/pull_request_creator/message_builder.rb, line 240
def updating_a_dependency_set?
  dependencies.first.
    requirements.
    any? { |r| r.dig(:metadata, :dependency_set) }
end
updating_a_property?() click to toggle source
# File lib/dependabot/pull_request_creator/message_builder.rb, line 234
def updating_a_property?
  dependencies.first.
    requirements.
    any? { |r| r.dig(:metadata, :property_name) }
end
upgrade_url(dependency) click to toggle source
# File lib/dependabot/pull_request_creator/message_builder.rb, line 346
def upgrade_url(dependency)
  metadata_finder(dependency).upgrade_guide_url
end
version_commit_message_intro() click to toggle source

rubocop:disable Metrics/PerceivedComplexity

# File lib/dependabot/pull_request_creator/message_builder.rb, line 181
def version_commit_message_intro
  return multidependency_property_intro if dependencies.count > 1 && updating_a_property?

  return dependency_set_intro if dependencies.count > 1 && updating_a_dependency_set?

  return multidependency_intro if dependencies.count > 1

  dependency = dependencies.first
  msg = "Bumps #{dependency_links.first} "\
        "#{from_version_msg(previous_version(dependency))}"\
        "to #{new_version(dependency)}."

  msg += " This release includes the previously tagged commit." if switching_from_ref_to_release?(dependency)

  if vulnerabilities_fixed[dependency.name]&.one?
    msg += " **This update includes a security fix.**"
  elsif vulnerabilities_fixed[dependency.name]&.any?
    msg += " **This update includes security fixes.**"
  end

  msg
end