class Dependabot::MetadataFinders::Base::CommitsFinder

Attributes

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

Public Class Methods

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

Public Instance Methods

commits() click to toggle source
# File lib/dependabot/metadata_finders/base/commits_finder.rb, line 38
def commits
  return [] unless source
  return [] unless new_tag && previous_tag

  case source.provider
  when "github" then fetch_github_commits
  when "bitbucket" then fetch_bitbucket_commits
  when "gitlab" then fetch_gitlab_commits
  when "azure" then [] # TODO: Fetch Azure commits
  else raise "Unexpected source provider '#{source.provider}'"
  end
end
commits_url() click to toggle source
# File lib/dependabot/metadata_finders/base/commits_finder.rb, line 23
def commits_url
  return unless source
  return if source.provider == "azure" # TODO: Fetch Azure commits

  path =
    case source.provider
    when "github" then github_compare_path(new_tag, previous_tag)
    when "bitbucket" then bitbucket_compare_path(new_tag, previous_tag)
    when "gitlab" then gitlab_compare_path(new_tag, previous_tag)
    else raise "Unexpected source provider '#{source.provider}'"
    end

  "#{source.url}/#{path}"
end
new_tag() click to toggle source
# File lib/dependabot/metadata_finders/base/commits_finder.rb, line 51
def new_tag
  new_version = dependency.version

  return new_version if git_source?(dependency.requirements) && git_sha?(new_version)

  return new_ref if new_ref && ref_changed?

  tags = dependency_tags.
         select { |tag| tag_matches_version?(tag, new_version) }.
         sort_by(&:length)

  tags.find { |t| t.include?(dependency.name) } || tags.first
end

Private Instance Methods

bitbucket_client() click to toggle source
# File lib/dependabot/metadata_finders/base/commits_finder.rb, line 293
def bitbucket_client
  @bitbucket_client ||= Dependabot::Clients::BitbucketWithRetries.
                        for_bitbucket_dot_org(credentials: credentials)
end
bitbucket_compare_path(new_tag, previous_tag) click to toggle source
# File lib/dependabot/metadata_finders/base/commits_finder.rb, line 197
def bitbucket_compare_path(new_tag, previous_tag)
  if new_tag && previous_tag
    "branches/compare/#{new_tag}..#{previous_tag}"
  elsif new_tag
    "commits/tag/#{new_tag}"
  else
    "commits"
  end
end
dependency_tags() click to toggle source
# File lib/dependabot/metadata_finders/base/commits_finder.rb, line 166
def dependency_tags
  @dependency_tags ||= fetch_dependency_tags
end
fetch_bitbucket_commits() click to toggle source
# File lib/dependabot/metadata_finders/base/commits_finder.rb, line 249
def fetch_bitbucket_commits
  bitbucket_client.
    compare(source.repo, previous_tag, new_tag).
    map do |commit|
      {
        message: commit.dig("summary", "raw"),
        sha: commit["hash"],
        html_url: commit.dig("links", "html", "href")
      }
    end
rescue Dependabot::Clients::Bitbucket::NotFound,
       Dependabot::Clients::Bitbucket::Unauthorized,
       Dependabot::Clients::Bitbucket::Forbidden,
       Excon::Error::Server,
       Excon::Error::Socket,
       Excon::Error::Timeout
  []
end
fetch_dependency_tags() click to toggle source
# File lib/dependabot/metadata_finders/base/commits_finder.rb, line 170
def fetch_dependency_tags
  return [] unless source

  GitMetadataFetcher.
    new(url: source.url, credentials: credentials).
    tags.
    map(&:name)
rescue Dependabot::GitDependenciesNotReachable,
       Octokit::ServiceUnavailable
  # ServiceUnavailable normally means a DMCA takedown
  []
end
fetch_github_commits() click to toggle source
# File lib/dependabot/metadata_finders/base/commits_finder.rb, line 217
def fetch_github_commits
  commits =
    begin
      # If part of a monorepo we make two requests in order to get only
      # the commits relevant to the given path
      path = source.directory&.gsub(%r{^[./]+}, "")
      repo = source.repo

      args = { sha: previous_tag, path: path }.compact
      previous_commit_shas =
        github_client.commits(repo, **args).map(&:sha)

      # NOTE: We reverse this so it's consistent with the array we get
      # from `github_client.compare(...)`
      args = { sha: new_tag, path: path }.compact
      github_client.
        commits(repo, **args).
        reject { |c| previous_commit_shas.include?(c.sha) }.reverse
    end
  return [] unless commits

  commits.map do |commit|
    {
      message: commit.commit.message,
      sha: commit.sha,
      html_url: commit.html_url
    }
  end
rescue Octokit::NotFound
  []
end
fetch_gitlab_commits() click to toggle source
# File lib/dependabot/metadata_finders/base/commits_finder.rb, line 268
def fetch_gitlab_commits
  gitlab_client.
    compare(source.repo, previous_tag, new_tag).
    commits.
    map do |commit|
      {
        message: commit["message"],
        sha: commit["id"],
        html_url: "#{source.url}/commit/#{commit['id']}"
      }
    end
rescue Gitlab::Error::NotFound
  []
end
git_sha?(version) click to toggle source
# File lib/dependabot/metadata_finders/base/commits_finder.rb, line 314
def git_sha?(version)
  return false unless version

  version.match?(/^[0-9a-f]{40}$/)
end
git_source?(requirements) click to toggle source

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

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

  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/commits_finder.rb, line 288
def github_client
  @github_client ||= Dependabot::Clients::GithubWithRetries.
                     for_github_dot_com(credentials: credentials)
end
github_compare_path(new_tag, previous_tag) click to toggle source
# File lib/dependabot/metadata_finders/base/commits_finder.rb, line 183
def github_compare_path(new_tag, previous_tag)
  if part_of_monorepo?
    # If part of a monorepo then we're better off linking to the commits
    # for that directory than trying to put together a compare URL
    Pathname.
      new(File.join("commits/#{new_tag || 'HEAD'}", source.directory)).
      cleanpath.to_path
  elsif new_tag && previous_tag
    "compare/#{previous_tag}...#{new_tag}"
  else
    new_tag ? "commits/#{new_tag}" : "commits"
  end
end
gitlab_client() click to toggle source
# File lib/dependabot/metadata_finders/base/commits_finder.rb, line 283
def gitlab_client
  @gitlab_client ||= Dependabot::Clients::GitlabWithRetries.
                     for_gitlab_dot_com(credentials: credentials)
end
gitlab_compare_path(new_tag, previous_tag) click to toggle source
# File lib/dependabot/metadata_finders/base/commits_finder.rb, line 207
def gitlab_compare_path(new_tag, previous_tag)
  if new_tag && previous_tag
    "compare/#{previous_tag}...#{new_tag}"
  elsif new_tag
    "commits/#{new_tag}"
  else
    "commits/master"
  end
end
lowest_tag_satisfying_previous_requirements() click to toggle source

rubocop:enable Metrics/PerceivedComplexity

# File lib/dependabot/metadata_finders/base/commits_finder.rb, line 89
def lowest_tag_satisfying_previous_requirements
  tags = dependency_tags.
         select { |t| version_from_tag(t) }.
         select { |t| satisfies_previous_reqs?(version_from_tag(t)) }.
         sort_by { |t| [version_from_tag(t), t.length] }

  tags.find { |t| t.include?(dependency.name) } || tags.first
end
new_ref() click to toggle source
# File lib/dependabot/metadata_finders/base/commits_finder.rb, line 143
def new_ref
  return unless git_source?(dependency.previous_requirements)

  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
part_of_monorepo?() click to toggle source
# File lib/dependabot/metadata_finders/base/commits_finder.rb, line 298
def part_of_monorepo?
  return false unless reliable_source_directory?

  ![nil, ".", "/"].include?(source.directory)
end
previous_ref() click to toggle source
# File lib/dependabot/metadata_finders/base/commits_finder.rb, line 134
def previous_ref
  return unless git_source?(dependency.previous_requirements)

  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_tag() click to toggle source

rubocop:disable Metrics/PerceivedComplexity

# File lib/dependabot/metadata_finders/base/commits_finder.rb, line 68
def previous_tag
  previous_version = dependency.previous_version

  if git_source?(dependency.previous_requirements) &&
     git_sha?(previous_version)
    previous_version
  elsif previous_ref && ref_changed?
    previous_ref
  elsif previous_version
    tags = dependency_tags.
           select { |tag| tag_matches_version?(tag, previous_version) }.
           sort_by(&:length)

    tags.find { |t| t.include?(dependency.name) } || tags.first
  elsif !git_source?(dependency.previous_requirements)
    lowest_tag_satisfying_previous_requirements
  end
end
ref_changed?() click to toggle source
# File lib/dependabot/metadata_finders/base/commits_finder.rb, line 129
def ref_changed?
  # We could go from multiple previous refs (nil) to a single new ref
  previous_ref != new_ref
end
reliable_source_directory?() click to toggle source
# File lib/dependabot/metadata_finders/base/commits_finder.rb, line 320
def reliable_source_directory?
  MetadataFinders::Base::PACKAGE_MANAGERS_WITH_RELIABLE_DIRECTORIES.
    include?(dependency.package_manager)
end
requirement_class() click to toggle source
# File lib/dependabot/metadata_finders/base/commits_finder.rb, line 308
def requirement_class
  Utils.requirement_class_for_package_manager(
    dependency.package_manager
  )
end
satisfies_previous_reqs?(version) click to toggle source
# File lib/dependabot/metadata_finders/base/commits_finder.rb, line 107
def satisfies_previous_reqs?(version)
  dependency.previous_requirements.all? do |req|
    next true unless req.fetch(:requirement)

    requirement_class.
      requirements_array(req.fetch(:requirement)).
      all? { |r| r.satisfied_by?(version) }
  end
end
tag_matches_version?(tag, version) click to toggle source
# File lib/dependabot/metadata_finders/base/commits_finder.rb, line 152
def tag_matches_version?(tag, version)
  return false unless version

  return tag.match?(/(?:[^0-9\.]|\A)#{Regexp.escape(version)}\z/) unless version_class.correct?(version)

  version_regex = GitCommitChecker::VERSION_REGEX
  return false unless tag.match?(version_regex)

  tag_version = tag.match(version_regex).named_captures.fetch("version")
  return false unless version_class.correct?(tag_version)

  version_class.new(tag_version) == version_class.new(version)
end
version_class() click to toggle source
# File lib/dependabot/metadata_finders/base/commits_finder.rb, line 304
def version_class
  Utils.version_class_for_package_manager(dependency.package_manager)
end
version_from_tag(tag) click to toggle source
# File lib/dependabot/metadata_finders/base/commits_finder.rb, line 98
def version_from_tag(tag)
  version_class.new(tag.gsub(/^v/, "")) if version_class.correct?(tag.gsub(/^v/, ""))

  return unless tag.gsub(/^[^\d]*/, "").length > 1
  return unless version_class.correct?(tag.gsub(/^[^\d]*/, ""))

  version_class.new(tag.gsub(/^[^\d]*/, ""))
end