Repository URL to install this package:
Version:
8.5.0.pre.alpha.4 ▾
|
# coding: utf-8
require "danger/helpers/comments_helper"
require "danger/request_sources/vsts_api"
module Danger
module RequestSources
class VSTS < RequestSource
include Danger::Helpers::CommentsHelper
attr_accessor :pr_json
def self.env_vars
[
"DANGER_VSTS_API_TOKEN",
"DANGER_VSTS_HOST"
]
end
def self.optional_env_vars
[
"DANGER_VSTS_API_VERSION"
]
end
def initialize(ci_source, environment)
self.ci_source = ci_source
@is_vsts_git = environment["BUILD_REPOSITORY_PROVIDER"] == "TfsGit"
project, slug = ci_source.repo_slug.split("/")
@api = VSTSAPI.new(project, slug, ci_source.pull_request_id, environment)
end
def validates_as_ci?
@is_vsts_git
end
def validates_as_api_source?
@api.credentials_given?
end
def scm
@scm ||= GitRepo.new
end
def client
@api
end
def host
@host ||= @api.host
end
def fetch_details
self.pr_json = @api.fetch_pr_json
end
def setup_danger_branches
base_branch = self.pr_json[:targetRefName].sub("refs/heads/", "")
base_commit = self.pr_json[:lastMergeTargetCommit][:commitId]
head_branch = self.pr_json[:sourceRefName].sub("refs/heads/", "")
head_commit = self.pr_json[:lastMergeSourceCommit][:commitId]
# Next, we want to ensure that we have a version of the current branch at a known location
scm.ensure_commitish_exists_on_branch! base_branch, base_commit
self.scm.exec "branch #{EnvironmentManager.danger_base_branch} #{base_commit}"
# OK, so we want to ensure that we have a known head branch, this will always represent
# the head of the PR ( e.g. the most recent commit that will be merged. )
scm.ensure_commitish_exists_on_branch! head_branch, head_commit
self.scm.exec "branch #{EnvironmentManager.danger_head_branch} #{head_commit}"
end
def organisation
nil
end
def update_pull_request!(warnings: [], errors: [], messages: [], markdowns: [], danger_id: "danger",
new_comment: false, remove_previous_comments: false)
unless @api.supports_comments?
return
end
regular_violations = regular_violations_group(
warnings: warnings,
errors: errors,
messages: messages,
markdowns: markdowns
)
inline_violations = inline_violations_group(
warnings: warnings,
errors: errors,
messages: messages,
markdowns: markdowns
)
rest_inline_violations = submit_inline_comments!(**{
danger_id: danger_id,
previous_violations: {}
}.merge(inline_violations))
main_violations = merge_violations(
regular_violations, rest_inline_violations
)
comment = generate_description(warnings: main_violations[:warnings], errors: main_violations[:errors])
comment += "\n\n"
comment += generate_comment(**{
previous_violations: {},
danger_id: danger_id,
template: "vsts"
}.merge(main_violations))
if new_comment || remove_previous_comments
post_new_comment(comment)
else
update_old_comment(comment, danger_id: danger_id)
end
end
def post_new_comment(comment)
@api.post_comment(comment)
end
def update_old_comment(new_comment, danger_id: "danger")
comment_updated = false
@api.fetch_last_comments.each do |c|
thread_id = c[:id]
comment = c[:comments].first
comment_id = comment[:id]
comment_content = comment[:content].nil? ? "" : comment[:content]
# Skip the comment if it wasn't posted by danger
next unless comment_content.include?("generated_by_#{danger_id}")
# Skip the comment if it's an inline comment
next unless c[:threadContext].nil?
# Updated the danger posted comment
@api.update_comment(thread_id, comment_id, new_comment)
comment_updated = true
end
# If no comment was updated, post a new one
post_new_comment(new_comment) unless comment_updated
end
def submit_inline_comments!(warnings: [], errors: [], messages: [], markdowns: [], previous_violations: [], danger_id: "danger")
# Avoid doing any fetchs if there's no inline comments
return {} if (warnings + errors + messages + markdowns).select(&:inline?).empty?
pr_threads = @api.fetch_last_comments
danger_threads = pr_threads.select do |thread|
comment = thread[:comments].first
comment_content = comment[:content].nil? ? "" : comment[:content]
next comment_content.include?("generated_by_#{danger_id}")
end
non_danger_threads = pr_threads - danger_threads
warnings = submit_inline_comments_for_kind!(:warning, warnings, danger_threads, previous_violations["warning"], danger_id: danger_id)
errors = submit_inline_comments_for_kind!(:error, errors, danger_threads, previous_violations["error"], danger_id: danger_id)
messages = submit_inline_comments_for_kind!(:message, messages, danger_threads, previous_violations["message"], danger_id: danger_id)
markdowns = submit_inline_comments_for_kind!(:markdown, markdowns, danger_threads, [], danger_id: danger_id)
# submit removes from the array all comments that are still in force
# so we strike out all remaining ones
danger_threads.each do |thread|
table = Danger::Helpers::MarkdownCommentsParsingHelper.parse_tables_from_comment(thread[:comments].first[:content]).first
violation = Danger::Helpers::MarkdownCommentsParsingHelper.violations_from_table(table).flatten.first
if !violation.nil? && violation.sticky
puts "Resolved violation found: #{violation.message}"
body = generate_inline_comment_body("white_check_mark", violation, danger_id: danger_id, resolved: true, template: "github")
@api.update_comment(thread[:id], thread[:comments].first[:id], body)
end
end
{
warnings: warnings,
errors: errors,
messages: messages,
markdowns: markdowns
}
end
def messages_are_equivalent(m1, m2)
blob_regexp = %r{blob/[0-9a-z]+/}
m1.file == m2.file && m1.line == m2.line &&
m1.message.sub(blob_regexp, "") == m2.message.sub(blob_regexp, "")
end
def submit_inline_comments_for_kind!(kind, messages, danger_threads, previous_violations, danger_id: "danger")
previous_violations ||= []
is_markdown_content = kind == :markdown
emoji = { warning: "warning", error: "no_entry_sign", message: "book" }[kind]
messages.reject do |m|
next false unless m.file && m.line
# Once we know we're gonna submit it, we format it
if is_markdown_content
body = generate_inline_markdown_body(m, danger_id: danger_id, template: "vsts")
else
# Hide the inline link behind a span
m.message.gsub!("\n", "<br />")
m = process_markdown(m, true)
body = generate_inline_comment_body(emoji, m, danger_id: danger_id, template: "vsts")
# A comment might be in previous_violations because only now it's part of the unified diff
# We remove from the array since it won't have a place in the table anymore
previous_violations.reject! { |v| messages_are_equivalent(v, m) }
end
matching_threads = danger_threads.select do |comment_data|
if comment_data.key?(:threadContext) && !comment_data[:threadContext].nil? &&
(comment_data[:threadContext][:filePath] == m.file ||
comment_data[:threadContext][:filePath] == "/#{m.file}") &&
comment_data[:threadContext].key?(:rightFileStart) &&
comment_data[:threadContext][:rightFileStart][:line] == m.line
# Parse it to avoid problems with strikethrough
violation = violations_from_table(comment_data[:comments].first[:content]).first
if violation
messages_are_equivalent(violation, m)
else
blob_regexp = %r{blob/[0-9a-z]+/}
comment_data[:comments].first[:content].sub(blob_regexp, "") == body.sub(blob_regexp, "")
end
else
false
end
end
if matching_threads.empty?
@api.post_inline_comment(body, m.file, m.line)
# Not reject because this comment has not completed
next false
else
# Remove the surviving comment so we don't strike it out
danger_threads.reject! { |c| matching_threads.include? c }
# Update the comment to remove the strikethrough if present
thread = matching_threads.first
@api.update_comment(thread[:id], thread[:comments].first[:id], body)
end
# Remove this element from the array
next true
end
end
private
def regular_violations_group(warnings: [], errors: [], messages: [], markdowns: [])
{
warnings: warnings.reject(&:inline?),
errors: errors.reject(&:inline?),
messages: messages.reject(&:inline?),
markdowns: markdowns.reject(&:inline?)
}
end
def inline_violations_group(warnings: [], errors: [], messages: [], markdowns: [])
cmp = proc do |a, b|
next -1 unless a.file && a.line
next 1 unless b.file && b.line
next a.line <=> b.line if a.file == b.file
next a.file <=> b.file
end
# Sort to group inline comments by file
{
warnings: warnings.select(&:inline?).sort(&cmp),
errors: errors.select(&:inline?).sort(&cmp),
messages: messages.select(&:inline?).sort(&cmp),
markdowns: markdowns.select(&:inline?).sort(&cmp)
}
end
def merge_violations(*violation_groups)
violation_groups.inject({}) do |accumulator, group|
accumulator.merge(group) { |_, old, fresh| old + fresh }
end
end
end
end
end