Skip to content

Commit

Permalink
Merge pull request dependabot#1211 from chris5287/feature/azure-devops
Browse files Browse the repository at this point in the history
Azure DevOps Client
  • Loading branch information
greysteil authored Jun 27, 2019
2 parents 7b77eeb + 2b5555b commit f303472
Show file tree
Hide file tree
Showing 21 changed files with 1,224 additions and 5 deletions.
213 changes: 213 additions & 0 deletions common/lib/dependabot/clients/azure.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
# frozen_string_literal: true

require "dependabot/shared_helpers"
require "excon"

module Dependabot
module Clients
class Azure
class NotFound < StandardError; end

#######################
# Constructor methods #
#######################

def self.for_source(source:, credentials:)
credential =
credentials.
select { |cred| cred["type"] == "git_source" }.
find { |cred| cred["host"] == source.hostname }

new(source, credential)
end

##########
# Client #
##########

def initialize(source, credentials)
@source = source
@credentials = credentials
end

def fetch_commit(_repo, branch)
response = get(source.api_endpoint +
source.organization + "/" + source.project +
"/_apis/git/repositories/" + source.unscoped_repo +
"/stats/branches?name=" + branch)

JSON.parse(response.body).fetch("commit").fetch("commitId")
end

def fetch_default_branch(_repo)
response = get(source.api_endpoint +
source.organization + "/" + source.project +
"/_apis/git/repositories/" + source.unscoped_repo)

JSON.parse(response.body).fetch("defaultBranch").gsub("refs/heads/", "")
end

def fetch_repo_contents(commit = nil, path = nil)
tree = fetch_repo_contents_treeroot(commit, path)

response = get(source.api_endpoint +
source.organization + "/" + source.project +
"/_apis/git/repositories/" + source.unscoped_repo +
"/trees/" + tree + "?recursive=false")

JSON.parse(response.body).fetch("treeEntries")
end

def fetch_repo_contents_treeroot(commit = nil, path = nil)
actual_path = path
actual_path = "/" if path.to_s.empty?

tree_url = source.api_endpoint +
source.organization + "/" + source.project +
"/_apis/git/repositories/" + source.unscoped_repo +
"/items?path=" + actual_path

unless commit.to_s.empty?
tree_url += "&versionDescriptor.versionType=commit" \
"&versionDescriptor.version=" + commit
end

tree_response = get(tree_url)

JSON.parse(tree_response.body).fetch("objectId")
end

def fetch_file_contents(commit, path)
response = get(source.api_endpoint +
source.organization + "/" + source.project +
"/_apis/git/repositories/" + source.unscoped_repo +
"/items?path=" + path +
"&versionDescriptor.versionType=commit" \
"&versionDescriptor.version=" + commit)

response.body
end

def commits(branch_name = nil)
commits_url = source.api_endpoint +
source.organization + "/" + source.project +
"/_apis/git/repositories/" + source.unscoped_repo +
"/commits"

unless branch_name.to_s.empty?
commits_url += "?searchCriteria.itemVersion.version=" + branch_name
end

response = get(commits_url)

JSON.parse(response.body).fetch("value")
end

def branch(branch_name)
response = get(source.api_endpoint +
source.organization + "/" + source.project +
"/_apis/git/repositories/" + source.unscoped_repo +
"/refs?filter=heads/" + branch_name)

JSON.parse(response.body).fetch("value").first
end

def pull_requests(source_branch, target_branch)
response = get(source.api_endpoint +
source.organization + "/" + source.project +
"/_apis/git/repositories/" + source.unscoped_repo +
"/pullrequests?searchCriteria.status=all" \
"&searchCriteria.sourceRefName=refs/heads/" + source_branch +
"&searchCriteria.targetRefName=refs/heads/" + target_branch)

JSON.parse(response.body).fetch("value")
end

def create_commit(branch_name, base_commit, commit_message, files)
content = {
refUpdates: [
{ name: "refs/heads/" + branch_name, oldObjectId: base_commit }
],
commits: [
{
comment: commit_message,
changes: files.map do |file|
{
changeType: "edit",
item: { path: file.path },
newContent: {
content: Base64.encode64(file.content),
contentType: "base64encoded"
}
}
end
}
]
}

post(source.api_endpoint + source.organization + "/" + source.project +
"/_apis/git/repositories/" + source.unscoped_repo +
"/pushes?api-version=5.0", content.to_json)
end

def create_pull_request(pr_name, source_branch, target_branch,
pr_description, labels)
# Azure DevOps only support descriptions up to 4000 characters (https://developercommunity.visualstudio.com/content/problem/608770/remove-4000-character-limit-on-pull-request-descri.html)
azure_max_length = 3999
if pr_description.length > azure_max_length
truncated_msg = "...\n\n_Description has been truncated_"
truncate_length = azure_max_length - truncated_msg.length
pr_description = pr_description[0..truncate_length] + truncated_msg
end

content = {
sourceRefName: "refs/heads/" + source_branch,
targetRefName: "refs/heads/" + target_branch,
title: pr_name,
description: pr_description,
labels: labels.map { |label| { name: label } }
}

post(source.api_endpoint +
source.organization + "/" + source.project +
"/_apis/git/repositories/" + source.unscoped_repo +
"/pullrequests?api-version=5.0", content.to_json)
end

def get(url)
response = Excon.get(
url,
user: credentials&.fetch("username"),
password: credentials&.fetch("password"),
idempotent: true,
**SharedHelpers.excon_defaults
)
raise NotFound if response.status == 404

response
end

def post(url, json)
response = Excon.post(
url,
headers: {
"Content-Type" => "application/json"
},
body: json,
user: credentials&.fetch("username"),
password: credentials&.fetch("password"),
idempotent: true,
**SharedHelpers.excon_defaults
)
raise NotFound if response.status == 404

response
end

private

attr_reader :credentials
attr_reader :source
end
end
end
32 changes: 32 additions & 0 deletions common/lib/dependabot/file_fetchers/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require "dependabot/dependency_file"
require "dependabot/source"
require "dependabot/errors"
require "dependabot/clients/azure"
require "dependabot/clients/github_with_retries"
require "dependabot/clients/bitbucket_with_retries"
require "dependabot/clients/gitlab_with_retries"
Expand All @@ -17,6 +18,7 @@ class Base
CLIENT_NOT_FOUND_ERRORS = [
Octokit::NotFound,
Gitlab::Error::NotFound,
Dependabot::Clients::Azure::NotFound,
Dependabot::Clients::Bitbucket::NotFound
].freeze

Expand Down Expand Up @@ -144,6 +146,8 @@ def _fetch_repo_contents_fully_specified(provider, repo, path, commit)
_github_repo_contents(repo, path, commit)
when "gitlab"
_gitlab_repo_contents(repo, path, commit)
when "azure"
_azure_repo_contents(path, commit)
when "bitbucket"
_bitbucket_repo_contents(repo, path, commit)
else raise "Unsupported provider '#{provider}'."
Expand Down Expand Up @@ -214,6 +218,25 @@ def _gitlab_repo_contents(repo, path, commit)
end
end

def _azure_repo_contents(path, commit)
response = azure_client.fetch_repo_contents(commit, path)

response.map do |entry|
type = case entry.fetch("gitObjectType")
when "blob" then "file"
when "tree" then "dir"
else entry.fetch("gitObjectType")
end

OpenStruct.new(
name: File.basename(entry.fetch("relativePath")),
path: entry.fetch("relativePath"),
type: type,
size: entry.fetch("size")
)
end
end

def _bitbucket_repo_contents(repo, path, commit)
response = bitbucket_client.fetch_repo_contents(
repo,
Expand Down Expand Up @@ -289,6 +312,8 @@ def _fetch_file_content_fully_specified(provider, repo, path, commit)
when "gitlab"
tmp = gitlab_client.get_file(repo, path, commit).content
Base64.decode64(tmp).force_encoding("UTF-8").encode
when "azure"
azure_client.fetch_file_contents(commit, path)
when "bitbucket"
bitbucket_client.fetch_file_contents(repo, commit, path)
else raise "Unsupported provider '#{source.provider}'."
Expand Down Expand Up @@ -362,6 +387,7 @@ def client_for_provider
case source.provider
when "github" then github_client
when "gitlab" then gitlab_client
when "azure" then azure_client
when "bitbucket" then bitbucket_client
else raise "Unsupported provider '#{source.provider}'."
end
Expand All @@ -383,6 +409,12 @@ def gitlab_client
)
end

def azure_client
@azure_client ||=
Dependabot::Clients::Azure.
for_source(source: source, credentials: credentials)
end

def bitbucket_client
# TODO: When self-hosted Bitbucket is supported this should use
# `Bitbucket.for_source`
Expand Down
16 changes: 16 additions & 0 deletions common/lib/dependabot/pull_request_creator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

module Dependabot
class PullRequestCreator
require "dependabot/pull_request_creator/azure"
require "dependabot/pull_request_creator/github"
require "dependabot/pull_request_creator/gitlab"
require "dependabot/pull_request_creator/message_builder"
Expand Down Expand Up @@ -68,6 +69,7 @@ def create
case source.provider
when "github" then github_creator.create
when "gitlab" then gitlab_creator.create
when "azure" then azure_creator.create
else raise "Unsupported provider #{source.provider}"
end
end
Expand Down Expand Up @@ -120,6 +122,20 @@ def gitlab_creator
)
end

def azure_creator
Azure.new(
source: source,
branch_name: branch_namer.new_branch_name,
base_commit: base_commit,
credentials: credentials,
files: files,
commit_message: message_builder.commit_message,
pr_description: message_builder.pr_message,
pr_name: message_builder.pr_name,
labeler: labeler
)
end

def message_builder
@message_builder ||
MessageBuilder.new(
Expand Down
Loading

0 comments on commit f303472

Please sign in to comment.