# typed: strict
# frozen_string_literal: true
require "aws-sdk-codecommit"
require "sorbet-runtime"
require "dependabot/shared_helpers"
module Dependabot
module Clients
class CodeCommit
extend T::Sig
class NotFound < StandardError; end
#######################
# Constructor methods #
#######################
sig do
params(
source: Dependabot::Source,
credentials: T::Array[Dependabot::Credential]
)
.returns(Dependabot::Clients::CodeCommit)
end
def self.for_source(source:, credentials:)
credential =
credentials
.select { |cred| cred["type"] == "git_source" }
.find { |cred| cred["region"] == source.hostname }
new(source, credential)
end
##########
# Client #
##########
sig do
params(
source: Dependabot::Source,
credentials: T.nilable(Dependabot::Credential)
)
.void
end
def initialize(source, credentials)
@source = source
@cc_client =
T.let(
if credentials
Aws::CodeCommit::Client.new(
access_key_id: credentials.fetch("username"),
secret_access_key: credentials.fetch("password"),
region: credentials.fetch("region")
)
else
Aws::CodeCommit::Client.new
end,
Aws::CodeCommit::Client
)
end
# TODO: Should repo be required?
sig { params(repo: T.nilable(String), branch: String).returns(String) }
def fetch_commit(repo, branch)
cc_client.get_branch(
branch_name: branch,
repository_name: repo
).branch.commit_id
end
sig { params(repo: String).returns(String) }
def fetch_default_branch(repo)
cc_client.get_repository(
repository_name: repo
).repository_metadata.default_branch
end
sig do
params(
repo: String, commit: T.nilable(String),
path: T.nilable(String)
)
# See PR 9344: should .returns(Seahorse::Client::Response)
# but it not extend Delegator, unblocking until shim or
# another fix is implemented
.returns(T.untyped)
end
def fetch_repo_contents(repo, commit = nil, path = nil)
actual_path = path
actual_path = "/" if path.to_s.empty?
cc_client.get_folder(
repository_name: repo,
commit_specifier: commit,
folder_path: actual_path
)
end
sig do
params(
repo: String,
commit: String,
path: String
)
.returns(String)
end
def fetch_file_contents(repo, commit, path)
cc_client.get_file(
repository_name: repo,
commit_specifier: commit,
file_path: path
).file_content
rescue Aws::CodeCommit::Errors::FileDoesNotExistException
raise NotFound
end
sig do
params(
branch_name: String
)
.returns(String)
end
def branch(branch_name)
cc_client.get_branch(
repository_name: source.unscoped_repo,
branch_name: branch_name
)
end
# work around b/c codecommit doesn't have a 'get all commits' api..
sig do
params(
repo: String,
branch_name: String,
result_count: Integer
)
.returns(T::Array[String])
end
def fetch_commits(repo, branch_name, result_count)
top_commit = fetch_commit(repo, branch_name)
retrieved_commits = []
pending_commits = []
# get the parent commit ids from the latest commit on the default branch
latest_commit = @cc_client.get_commit(
repository_name: repo,
commit_id: top_commit
)
# add the parent commit ids to the pending_commits array
pending_commits.push(*latest_commit.commit.parents)
# iterate over the array of pending commits and
# get each of the corresponding parent commits
until pending_commits.empty? || retrieved_commits.count > result_count
commit_id = pending_commits[0]
# get any parent commits from the provided commit
parent_commits = @cc_client.get_commit(
repository_name: repo,
commit_id: commit_id
)
# remove the previously retrieved_commits
# form the pending_commits array
pending_commits.delete(commit_id)
# add the commit id to the retrieved_commits array
retrieved_commits << commit_id
# add the retrieved parent commits to the pending_commits array
pending_commits.push(*parent_commits.commit.parents)
end
retrieved_commits << top_commit
result = retrieved_commits | pending_commits
result
end
sig do
params(
repo: String,
branch_name: String
)
.returns(Aws::CodeCommit::Types::Commit)
end
def commits(repo, branch_name = T.must(source.branch))
retrieved_commits = fetch_commits(repo, branch_name, 5)
result = @cc_client.batch_get_commits(
commit_ids: retrieved_commits,
repository_name: repo
)
# sort the results by date
result.commits.sort! { |a, b| b.author.date <=> a.author.date }
result
end
sig do
params(
repo: String,
state: String,
branch: String
)
.returns(T::Array[Aws::CodeCommit::Types::PullRequest])
end
def pull_requests(repo, state, branch)
pull_request_ids = @cc_client.list_pull_requests(
repository_name: repo,
pull_request_status: state
).pull_request_ids
result = []
# list_pull_requests only gets us the pull request id
# get_pull_request has all the info we need
pull_request_ids.each do |id|
pr_hash = @cc_client.get_pull_request(
pull_request_id: id
)
# only include PRs from the referenced branch
if pr_hash.pull_request.pull_request_targets[0]
.source_reference.include? branch
result << pr_hash
end
end
result
end
sig do
params(
repo: String,
branch_name: String,
commit_id: String
)
.returns(Aws::CodeCommit::Types::BranchInfo)
end
def create_branch(repo, branch_name, commit_id)
cc_client.create_branch(
repository_name: repo,
branch_name: branch_name,
commit_id: commit_id
)
end
sig do
params(
branch_name: String,
author_name: T.nilable(String),
base_commit: String,
commit_message: String,
files: T::Array[Dependabot::DependencyFile]
)
.returns(Aws::CodeCommit::Types::CreateCommitOutput)
end
def create_commit(branch_name, author_name, base_commit, commit_message,
files)
cc_client.create_commit(
repository_name: source.unscoped_repo,
branch_name: branch_name,
parent_commit_id: base_commit,
author_name: author_name,
commit_message: commit_message,
put_files: files.map do |file|
{
file_path: file.path,
file_mode: "NORMAL",
file_content: file.content
}
end
)
end
sig do
params(
pr_name: String,
target_branch: String,
source_branch: String,
pr_description: String
)
.returns(T.nilable(Aws::CodeCommit::Types::CreatePullRequestOutput))
end
def create_pull_request(pr_name, target_branch, source_branch,
pr_description)
cc_client.create_pull_request(
title: pr_name,
description: pr_description,
targets: [
repository_name: source.unscoped_repo,
source_reference: target_branch,
destination_reference: source_branch
]
)
end
private
sig { returns(Dependabot::Source) }
attr_reader :source
sig { returns(Aws::CodeCommit::Client) }
attr_reader :cc_client
end
end
end