class Dependabot::MetadataFinders::Base::ChangelogFinder

Constants

CHANGELOG_NAMES

Earlier entries are preferred

Attributes

credentials[R]
dependency[R]
source[R]
suggested_changelog_url[R]

Public Class Methods

new(source:, dependency:, credentials:, suggested_changelog_url: nil) click to toggle source
# File lib/dependabot/metadata_finders/base/changelog_finder.rb, line 25
def initialize(source:, dependency:, credentials:,
               suggested_changelog_url: nil)
  @source = source
  @dependency = dependency
  @credentials = credentials
  @suggested_changelog_url = suggested_changelog_url
end

Public Instance Methods

changelog_text() click to toggle source
# File lib/dependabot/metadata_finders/base/changelog_finder.rb, line 37
def changelog_text
  return unless full_changelog_text

  pruned_text = ChangelogPruner.new(
    dependency: dependency,
    changelog_text: full_changelog_text
  ).pruned_text

  return pruned_text unless changelog.name.end_with?(".rst")

  begin
    PandocRuby.convert(
      pruned_text,
      from: :rst,
      to: :markdown,
      wrap: :none,
      timeout: 10
    )
  rescue Errno::ENOENT => e
    raise unless e.message == "No such file or directory - pandoc"

    # If pandoc isn't installed just return the rst
    pruned_text
  rescue RuntimeError => e
    raise unless e.message.include?("Pandoc timed out")

    pruned_text
  end
end
changelog_url() click to toggle source
# File lib/dependabot/metadata_finders/base/changelog_finder.rb, line 33
def changelog_url
  changelog&.html_url
end
upgrade_guide_text() click to toggle source
# File lib/dependabot/metadata_finders/base/changelog_finder.rb, line 71
def upgrade_guide_text
  return unless upgrade_guide

  @upgrade_guide_text ||= fetch_file_text(upgrade_guide)
end
upgrade_guide_url() click to toggle source
# File lib/dependabot/metadata_finders/base/changelog_finder.rb, line 67
def upgrade_guide_url
  upgrade_guide&.html_url
end

Private Instance Methods

bitbucket_client() click to toggle source
# File lib/dependabot/metadata_finders/base/changelog_finder.rb, line 371
def bitbucket_client
  @bitbucket_client ||= Dependabot::Clients::BitbucketWithRetries.
                        for_bitbucket_dot_org(credentials: credentials)
end
changelog() click to toggle source

rubocop:disable Metrics/PerceivedComplexity

# File lib/dependabot/metadata_finders/base/changelog_finder.rb, line 80
def changelog
  return unless changelog_from_suggested_url || source
  return if git_source? && !ref_changed?
  return changelog_from_suggested_url if changelog_from_suggested_url

  # If there is a changelog, and it includes the new version, return it
  if new_version && default_branch_changelog &&
     fetch_file_text(default_branch_changelog)&.include?(new_version)
    return default_branch_changelog
  end

  # Otherwise, look for a changelog at the tag for this version
  if new_version && relevant_tag_changelog &&
     fetch_file_text(relevant_tag_changelog)&.include?(new_version)
    return relevant_tag_changelog
  end

  # Fall back to the changelog (or nil) from the default branch
  default_branch_changelog
end
changelog_from_ref(ref) click to toggle source
# File lib/dependabot/metadata_finders/base/changelog_finder.rb, line 133
def changelog_from_ref(ref)
  files =
    dependency_file_list(ref).
    select { |f| f.type == "file" }.
    reject { |f| f.name.end_with?(".sh") }.
    reject { |f| f.size > 1_000_000 }.
    reject { |f| f.size < 100 }

  select_best_changelog(files)
end
changelog_from_suggested_url() click to toggle source

rubocop:enable Metrics/PerceivedComplexity

# File lib/dependabot/metadata_finders/base/changelog_finder.rb, line 102
def changelog_from_suggested_url
  return @changelog_from_suggested_url if defined?(@changelog_from_suggested_url)
  return unless suggested_changelog_url

  # TODO: Support other providers
  source = Source.from_url(suggested_changelog_url)
  return unless source&.provider == "github"

  opts = { path: source.directory, ref: source.branch }.compact
  tmp_files = github_client.contents(source.repo, opts)

  filename = suggested_changelog_url.split("/").last.split("#").first
  @changelog_from_suggested_url =
    tmp_files.find { |f| f.name == filename }
rescue Octokit::NotFound, Octokit::UnavailableForLegalReasons
  @changelog_from_suggested_url = nil
end
default_bitbucket_branch() click to toggle source
# File lib/dependabot/metadata_finders/base/changelog_finder.rb, line 376
def default_bitbucket_branch
  @default_bitbucket_branch ||=
    bitbucket_client.fetch_default_branch(source.repo)
end
default_branch_changelog() click to toggle source
# File lib/dependabot/metadata_finders/base/changelog_finder.rb, line 120
def default_branch_changelog
  return unless source

  @default_branch_changelog ||= changelog_from_ref(nil)
end
dependency_file_list(ref = nil) click to toggle source
# File lib/dependabot/metadata_finders/base/changelog_finder.rb, line 234
def dependency_file_list(ref = nil)
  @dependency_file_list ||= {}
  @dependency_file_list[ref] ||= fetch_dependency_file_list(ref)
end
fetch_bitbucket_file(file) click to toggle source
# File lib/dependabot/metadata_finders/base/changelog_finder.rb, line 215
def fetch_bitbucket_file(file)
  bitbucket_client.get(file.download_url).body.
    force_encoding("UTF-8").encode
end
fetch_bitbucket_file_list() click to toggle source
# File lib/dependabot/metadata_finders/base/changelog_finder.rb, line 273
def fetch_bitbucket_file_list
  branch = default_bitbucket_branch
  bitbucket_client.fetch_repo_contents(source.repo).map do |file|
    type = case file.fetch("type")
           when "commit_file" then "file"
           when "commit_directory" then "dir"
           else file.fetch("type")
           end
    OpenStruct.new(
      name: file.fetch("path").split("/").last,
      type: type,
      size: file.fetch("size", 100),
      html_url: "#{source.url}/src/#{branch}/#{file['path']}",
      download_url: "#{source.url}/raw/#{branch}/#{file['path']}"
    )
  end
rescue Dependabot::Clients::Bitbucket::NotFound,
       Dependabot::Clients::Bitbucket::Unauthorized,
       Dependabot::Clients::Bitbucket::Forbidden
  []
end
fetch_dependency_file_list(ref) click to toggle source
# File lib/dependabot/metadata_finders/base/changelog_finder.rb, line 239
def fetch_dependency_file_list(ref)
  case source.provider
  when "github" then fetch_github_file_list(ref)
  when "bitbucket" then fetch_bitbucket_file_list
  when "gitlab" then fetch_gitlab_file_list
  when "azure" then [] # TODO: Fetch files from Azure
  else raise "Unexpected repo provider '#{source.provider}'"
  end
end
fetch_file_text(file) click to toggle source
# File lib/dependabot/metadata_finders/base/changelog_finder.rb, line 182
def fetch_file_text(file)
  @file_text ||= {}

  unless @file_text.key?(file.download_url)
    provider = Source.from_url(file.html_url).provider
    @file_text[file.download_url] =
      case provider
      when "github" then fetch_github_file(file)
      when "gitlab" then fetch_gitlab_file(file)
      when "bitbucket" then fetch_bitbucket_file(file)
      else raise "Unsupported provider '#{provider}'"
      end
  end

  return unless @file_text[file.download_url].valid_encoding?

  @file_text[file.download_url].sub(/\n*\z/, "")
end
fetch_github_file(file) click to toggle source
# File lib/dependabot/metadata_finders/base/changelog_finder.rb, line 201
def fetch_github_file(file)
  # Hitting the download URL directly causes encoding problems
  raw_content = github_client.get(file.url).content
  Base64.decode64(raw_content).force_encoding("UTF-8").encode
end
fetch_github_file_list(ref) click to toggle source
# File lib/dependabot/metadata_finders/base/changelog_finder.rb, line 249
def fetch_github_file_list(ref)
  files = []

  if source.directory
    opts = { path: source.directory, ref: ref }.compact
    tmp_files = github_client.contents(source.repo, opts)
    files += tmp_files if tmp_files.is_a?(Array)
  end

  opts = { ref: ref }.compact
  files += github_client.contents(source.repo, opts)

  files.uniq.each do |f|
    next unless %w(doc docs).include?(f.name) && f.type == "dir"

    opts = { path: f.path, ref: ref }.compact
    files += github_client.contents(source.repo, opts)
  end

  files
rescue Octokit::NotFound, Octokit::UnavailableForLegalReasons
  []
end
fetch_gitlab_file(file) click to toggle source
# File lib/dependabot/metadata_finders/base/changelog_finder.rb, line 207
def fetch_gitlab_file(file)
  Excon.get(
    file.download_url,
    idempotent: true,
    **SharedHelpers.excon_defaults
  ).body.force_encoding("UTF-8").encode
end
fetch_gitlab_file_list() click to toggle source
# File lib/dependabot/metadata_finders/base/changelog_finder.rb, line 295
def fetch_gitlab_file_list
  gitlab_client.repo_tree(source.repo).map do |file|
    type = case file.type
           when "blob" then "file"
           when "tree" then "dir"
           else file.fetch("type")
           end
    OpenStruct.new(
      name: file.name,
      type: type,
      size: 100, # GitLab doesn't return file size
      html_url: "#{source.url}/blob/master/#{file.path}",
      download_url: "#{source.url}/raw/master/#{file.path}"
    )
  end
rescue Gitlab::Error::NotFound
  []
end
full_changelog_text() click to toggle source
# File lib/dependabot/metadata_finders/base/changelog_finder.rb, line 176
def full_changelog_text
  return unless changelog

  fetch_file_text(changelog)
end
git_source?() click to toggle source

TODO: Refactor me so that Composer doesn't need to be special cased

# File lib/dependabot/metadata_finders/base/changelog_finder.rb, line 341
def git_source?
  # Special case Composer, which uses git as a source but handles tags
  # internally
  return false if dependency.package_manager == "composer"

  requirements = dependency.requirements
  sources = requirements.map { |r| r.fetch(:source) }.uniq.compact
  return false if sources.empty?

  sources.all? { |s| s[:type] == "git" || s["type"] == "git" }
end
github_client() click to toggle source
# File lib/dependabot/metadata_finders/base/changelog_finder.rb, line 366
def github_client
  @github_client ||= Dependabot::Clients::GithubWithRetries.
                     for_github_dot_com(credentials: credentials)
end
gitlab_client() click to toggle source
# File lib/dependabot/metadata_finders/base/changelog_finder.rb, line 361
def gitlab_client
  @gitlab_client ||= Dependabot::Clients::GitlabWithRetries.
                     for_gitlab_dot_com(credentials: credentials)
end
major_version_upgrade?() click to toggle source
# File lib/dependabot/metadata_finders/base/changelog_finder.rb, line 353
def major_version_upgrade?
  return false unless dependency.version&.match?(/^\d/)
  return false unless dependency.previous_version&.match?(/^\d/)

  dependency.version.split(".").first.to_i -
    dependency.previous_version.split(".").first.to_i >= 1
end
new_ref() click to toggle source
# File lib/dependabot/metadata_finders/base/changelog_finder.rb, line 328
def new_ref
  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() click to toggle source
# File lib/dependabot/metadata_finders/base/changelog_finder.rb, line 314
def new_version
  return @new_version if defined?(@new_version)

  new_version = git_source? && new_ref ? new_ref : dependency.version
  @new_version = new_version&.gsub(/^v/, "")
end
previous_ref() click to toggle source
# File lib/dependabot/metadata_finders/base/changelog_finder.rb, line 321
def previous_ref
  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
ref_changed?() click to toggle source
# File lib/dependabot/metadata_finders/base/changelog_finder.rb, line 335
def ref_changed?
  # We could go from multiple previous refs (nil) to a single new ref
  previous_ref != new_ref
end
relevant_tag_changelog() click to toggle source
# File lib/dependabot/metadata_finders/base/changelog_finder.rb, line 126
def relevant_tag_changelog
  return unless source
  return unless tag_for_new_version

  @relevant_tag_changelog ||= changelog_from_ref(tag_for_new_version)
end
select_best_changelog(files) click to toggle source

rubocop:disable Metrics/PerceivedComplexity

# File lib/dependabot/metadata_finders/base/changelog_finder.rb, line 145
def select_best_changelog(files)
  CHANGELOG_NAMES.each do |name|
    candidates = files.select { |f| f.name =~ /#{name}/i }
    file = candidates.first if candidates.one?
    file ||=
      candidates.find do |f|
        candidates -= [f] && next if fetch_file_text(f).nil?
        pruner = ChangelogPruner.new(
          dependency: dependency,
          changelog_text: fetch_file_text(f)
        )
        pruner.includes_new_version? ||
          pruner.includes_previous_version?
      end
    file ||= candidates.max_by(&:size)
    return file if file
  end

  nil
end
tag_for_new_version() click to toggle source

rubocop:enable Metrics/PerceivedComplexity

# File lib/dependabot/metadata_finders/base/changelog_finder.rb, line 167
def tag_for_new_version
  @tag_for_new_version ||=
    CommitsFinder.new(
      dependency: dependency,
      source: source,
      credentials: credentials
    ).new_tag
end
upgrade_guide() click to toggle source
# File lib/dependabot/metadata_finders/base/changelog_finder.rb, line 220
def upgrade_guide
  return unless source

  # Upgrade guide usually won't be relevant for bumping anything other
  # than the major version
  return unless major_version_upgrade?

  dependency_file_list.
    select { |f| f.type == "file" }.
    select { |f| f.name.casecmp("upgrade.md").zero? }.
    reject { |f| f.size > 1_000_000 }.
    max_by(&:size)
end