danger 8.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +22 -0
- data/README.md +94 -0
- data/bin/danger +5 -0
- data/lib/assets/DangerfileTemplate +13 -0
- data/lib/danger.rb +44 -0
- data/lib/danger/ci_source/appcenter.rb +55 -0
- data/lib/danger/ci_source/appveyor.rb +60 -0
- data/lib/danger/ci_source/azure_pipelines.rb +44 -0
- data/lib/danger/ci_source/bamboo.rb +41 -0
- data/lib/danger/ci_source/bitbucket_pipelines.rb +37 -0
- data/lib/danger/ci_source/bitrise.rb +65 -0
- data/lib/danger/ci_source/buddybuild.rb +62 -0
- data/lib/danger/ci_source/buildkite.rb +51 -0
- data/lib/danger/ci_source/ci_source.rb +37 -0
- data/lib/danger/ci_source/circle.rb +94 -0
- data/lib/danger/ci_source/circle_api.rb +51 -0
- data/lib/danger/ci_source/cirrus.rb +31 -0
- data/lib/danger/ci_source/code_build.rb +57 -0
- data/lib/danger/ci_source/codefresh.rb +53 -0
- data/lib/danger/ci_source/codeship.rb +44 -0
- data/lib/danger/ci_source/dotci.rb +52 -0
- data/lib/danger/ci_source/drone.rb +71 -0
- data/lib/danger/ci_source/github_actions.rb +43 -0
- data/lib/danger/ci_source/gitlab_ci.rb +86 -0
- data/lib/danger/ci_source/jenkins.rb +149 -0
- data/lib/danger/ci_source/local_git_repo.rb +119 -0
- data/lib/danger/ci_source/local_only_git_repo.rb +47 -0
- data/lib/danger/ci_source/screwdriver.rb +47 -0
- data/lib/danger/ci_source/semaphore.rb +37 -0
- data/lib/danger/ci_source/support/commits.rb +17 -0
- data/lib/danger/ci_source/support/find_repo_info_from_logs.rb +35 -0
- data/lib/danger/ci_source/support/find_repo_info_from_url.rb +42 -0
- data/lib/danger/ci_source/support/local_pull_request.rb +14 -0
- data/lib/danger/ci_source/support/no_pull_request.rb +7 -0
- data/lib/danger/ci_source/support/no_repo_info.rb +5 -0
- data/lib/danger/ci_source/support/pull_request_finder.rb +179 -0
- data/lib/danger/ci_source/support/remote_pull_request.rb +15 -0
- data/lib/danger/ci_source/support/repo_info.rb +10 -0
- data/lib/danger/ci_source/surf.rb +37 -0
- data/lib/danger/ci_source/teamcity.rb +159 -0
- data/lib/danger/ci_source/travis.rb +51 -0
- data/lib/danger/ci_source/vsts.rb +73 -0
- data/lib/danger/ci_source/xcode_server.rb +48 -0
- data/lib/danger/clients/rubygems_client.rb +14 -0
- data/lib/danger/commands/dangerfile/gem.rb +43 -0
- data/lib/danger/commands/dangerfile/init.rb +30 -0
- data/lib/danger/commands/dry_run.rb +54 -0
- data/lib/danger/commands/init.rb +297 -0
- data/lib/danger/commands/init_helpers/interviewer.rb +92 -0
- data/lib/danger/commands/local.rb +83 -0
- data/lib/danger/commands/local_helpers/http_cache.rb +36 -0
- data/lib/danger/commands/local_helpers/local_setup.rb +46 -0
- data/lib/danger/commands/local_helpers/pry_setup.rb +31 -0
- data/lib/danger/commands/plugins/plugin_json.rb +46 -0
- data/lib/danger/commands/plugins/plugin_lint.rb +54 -0
- data/lib/danger/commands/plugins/plugin_readme.rb +45 -0
- data/lib/danger/commands/pr.rb +92 -0
- data/lib/danger/commands/runner.rb +94 -0
- data/lib/danger/commands/staging.rb +53 -0
- data/lib/danger/commands/systems.rb +43 -0
- data/lib/danger/comment_generators/bitbucket_server.md.erb +20 -0
- data/lib/danger/comment_generators/bitbucket_server_inline.md.erb +15 -0
- data/lib/danger/comment_generators/bitbucket_server_message_group.md.erb +12 -0
- data/lib/danger/comment_generators/github.md.erb +55 -0
- data/lib/danger/comment_generators/github_inline.md.erb +26 -0
- data/lib/danger/comment_generators/gitlab.md.erb +40 -0
- data/lib/danger/comment_generators/gitlab_inline.md.erb +26 -0
- data/lib/danger/comment_generators/vsts.md.erb +20 -0
- data/lib/danger/core_ext/file_list.rb +18 -0
- data/lib/danger/core_ext/string.rb +20 -0
- data/lib/danger/danger_core/dangerfile.rb +341 -0
- data/lib/danger/danger_core/dangerfile_dsl.rb +29 -0
- data/lib/danger/danger_core/dangerfile_generator.rb +11 -0
- data/lib/danger/danger_core/environment_manager.rb +123 -0
- data/lib/danger/danger_core/executor.rb +92 -0
- data/lib/danger/danger_core/message_aggregator.rb +49 -0
- data/lib/danger/danger_core/message_group.rb +68 -0
- data/lib/danger/danger_core/messages/base.rb +56 -0
- data/lib/danger/danger_core/messages/markdown.rb +42 -0
- data/lib/danger/danger_core/messages/violation.rb +54 -0
- data/lib/danger/danger_core/plugins/dangerfile_bitbucket_cloud_plugin.rb +144 -0
- data/lib/danger/danger_core/plugins/dangerfile_bitbucket_server_plugin.rb +211 -0
- data/lib/danger/danger_core/plugins/dangerfile_danger_plugin.rb +248 -0
- data/lib/danger/danger_core/plugins/dangerfile_git_plugin.rb +158 -0
- data/lib/danger/danger_core/plugins/dangerfile_github_plugin.rb +254 -0
- data/lib/danger/danger_core/plugins/dangerfile_gitlab_plugin.rb +240 -0
- data/lib/danger/danger_core/plugins/dangerfile_local_only_plugin.rb +42 -0
- data/lib/danger/danger_core/plugins/dangerfile_messaging_plugin.rb +218 -0
- data/lib/danger/danger_core/plugins/dangerfile_vsts_plugin.rb +191 -0
- data/lib/danger/danger_core/standard_error.rb +143 -0
- data/lib/danger/helpers/array_subclass.rb +61 -0
- data/lib/danger/helpers/comment.rb +32 -0
- data/lib/danger/helpers/comments_helper.rb +178 -0
- data/lib/danger/helpers/comments_parsing_helper.rb +70 -0
- data/lib/danger/helpers/emoji_mapper.rb +41 -0
- data/lib/danger/helpers/find_max_num_violations.rb +31 -0
- data/lib/danger/helpers/message_groups_array_helper.rb +31 -0
- data/lib/danger/plugin_support/gems_resolver.rb +77 -0
- data/lib/danger/plugin_support/plugin.rb +49 -0
- data/lib/danger/plugin_support/plugin_file_resolver.rb +30 -0
- data/lib/danger/plugin_support/plugin_linter.rb +161 -0
- data/lib/danger/plugin_support/plugin_parser.rb +199 -0
- data/lib/danger/plugin_support/templates/readme_table.html.erb +26 -0
- data/lib/danger/request_sources/bitbucket_cloud.rb +171 -0
- data/lib/danger/request_sources/bitbucket_cloud_api.rb +181 -0
- data/lib/danger/request_sources/bitbucket_server.rb +105 -0
- data/lib/danger/request_sources/bitbucket_server_api.rb +117 -0
- data/lib/danger/request_sources/github/github.rb +530 -0
- data/lib/danger/request_sources/github/github_review.rb +126 -0
- data/lib/danger/request_sources/github/github_review_resolver.rb +19 -0
- data/lib/danger/request_sources/github/github_review_unsupported.rb +25 -0
- data/lib/danger/request_sources/gitlab.rb +525 -0
- data/lib/danger/request_sources/local_only.rb +53 -0
- data/lib/danger/request_sources/request_source.rb +85 -0
- data/lib/danger/request_sources/support/get_ignored_violation.rb +17 -0
- data/lib/danger/request_sources/vsts.rb +118 -0
- data/lib/danger/request_sources/vsts_api.rb +138 -0
- data/lib/danger/scm_source/git_repo.rb +181 -0
- data/lib/danger/version.rb +4 -0
- metadata +339 -0
@@ -0,0 +1,117 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require "danger/helpers/comments_helper"
|
4
|
+
|
5
|
+
module Danger
|
6
|
+
module RequestSources
|
7
|
+
class BitbucketServerAPI
|
8
|
+
attr_accessor :host, :pr_api_endpoint, :key, :project
|
9
|
+
|
10
|
+
def initialize(project, slug, pull_request_id, environment)
|
11
|
+
@username = environment["DANGER_BITBUCKETSERVER_USERNAME"]
|
12
|
+
@password = environment["DANGER_BITBUCKETSERVER_PASSWORD"]
|
13
|
+
self.host = environment["DANGER_BITBUCKETSERVER_HOST"]
|
14
|
+
if self.host && !(self.host.include? "http://") && !(self.host.include? "https://")
|
15
|
+
self.host = "https://" + self.host
|
16
|
+
end
|
17
|
+
self.key = slug
|
18
|
+
self.project = project
|
19
|
+
self.pr_api_endpoint = "#{host}/rest/api/1.0/projects/#{project}/repos/#{slug}/pull-requests/#{pull_request_id}"
|
20
|
+
end
|
21
|
+
|
22
|
+
def inspect
|
23
|
+
inspected = super
|
24
|
+
|
25
|
+
if @password
|
26
|
+
inspected = inspected.sub! @password, "********".freeze
|
27
|
+
end
|
28
|
+
|
29
|
+
inspected
|
30
|
+
end
|
31
|
+
|
32
|
+
def credentials_given?
|
33
|
+
@username && !@username.empty? && @password && !@password.empty?
|
34
|
+
end
|
35
|
+
|
36
|
+
def pull_request(*)
|
37
|
+
fetch_pr_json
|
38
|
+
end
|
39
|
+
|
40
|
+
def fetch_pr_json
|
41
|
+
uri = URI(pr_api_endpoint)
|
42
|
+
fetch_json(uri)
|
43
|
+
end
|
44
|
+
|
45
|
+
def fetch_last_comments
|
46
|
+
uri = URI("#{pr_api_endpoint}/activities?limit=1000")
|
47
|
+
fetch_json(uri)[:values].select { |v| v[:action] == "COMMENTED" }.map { |v| v[:comment] }
|
48
|
+
end
|
49
|
+
|
50
|
+
def delete_comment(id, version)
|
51
|
+
uri = URI("#{pr_api_endpoint}/comments/#{id}?version=#{version}")
|
52
|
+
delete(uri)
|
53
|
+
end
|
54
|
+
|
55
|
+
def post_comment(text)
|
56
|
+
uri = URI("#{pr_api_endpoint}/comments")
|
57
|
+
body = { text: text }.to_json
|
58
|
+
post(uri, body)
|
59
|
+
end
|
60
|
+
|
61
|
+
def update_pr_build_status(status, changeset, build_job_link, description)
|
62
|
+
uri = URI("#{self.host}/rest/build-status/1.0/commits/#{changeset}")
|
63
|
+
body = build_status_body(status, build_job_link, description)
|
64
|
+
post(uri, body)
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def use_ssl
|
70
|
+
return self.pr_api_endpoint.include? "https://"
|
71
|
+
end
|
72
|
+
|
73
|
+
def fetch_json(uri)
|
74
|
+
req = Net::HTTP::Get.new(uri.request_uri, { "Content-Type" => "application/json" })
|
75
|
+
req.basic_auth @username, @password
|
76
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: use_ssl) do |http|
|
77
|
+
http.request(req)
|
78
|
+
end
|
79
|
+
JSON.parse(res.body, symbolize_names: true)
|
80
|
+
end
|
81
|
+
|
82
|
+
def post(uri, body)
|
83
|
+
req = Net::HTTP::Post.new(uri.request_uri, { "Content-Type" => "application/json" })
|
84
|
+
req.basic_auth @username, @password
|
85
|
+
req.body = body
|
86
|
+
|
87
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: use_ssl) do |http|
|
88
|
+
http.request(req)
|
89
|
+
end
|
90
|
+
|
91
|
+
# show error to the user when Bitbucket Server returned an error
|
92
|
+
case res
|
93
|
+
when Net::HTTPClientError, Net::HTTPServerError
|
94
|
+
# HTTP 4xx - 5xx
|
95
|
+
abort "\nError posting comment to Bitbucket Server: #{res.code} (#{res.message}) - #{res.body}\n\n"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def delete(uri)
|
100
|
+
req = Net::HTTP::Delete.new(uri.request_uri, { "Content-Type" => "application/json" })
|
101
|
+
req.basic_auth @username, @password
|
102
|
+
Net::HTTP.start(uri.hostname, uri.port, use_ssl: use_ssl) do |http|
|
103
|
+
http.request(req)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def build_status_body(status, build_job_link, description)
|
108
|
+
body = Hash.new
|
109
|
+
body["state"] = status
|
110
|
+
body["key"] = self.key
|
111
|
+
body["url"] = build_job_link
|
112
|
+
body["description"] = description if description
|
113
|
+
return body.to_json
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,530 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
# rubocop:disable Metrics/ClassLength
|
4
|
+
|
5
|
+
require "octokit"
|
6
|
+
require "danger/helpers/comments_helper"
|
7
|
+
require "danger/helpers/comment"
|
8
|
+
require "danger/request_sources/github/github_review"
|
9
|
+
require "danger/request_sources/github/github_review_unsupported"
|
10
|
+
require "danger/request_sources/support/get_ignored_violation"
|
11
|
+
|
12
|
+
module Danger
|
13
|
+
module RequestSources
|
14
|
+
class GitHub < RequestSource
|
15
|
+
include Danger::Helpers::CommentsHelper
|
16
|
+
|
17
|
+
attr_accessor :pr_json, :issue_json, :support_tokenless_auth, :dismiss_out_of_range_messages
|
18
|
+
|
19
|
+
def self.env_vars
|
20
|
+
["DANGER_GITHUB_API_TOKEN"]
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.optional_env_vars
|
24
|
+
["DANGER_GITHUB_HOST", "DANGER_GITHUB_API_BASE_URL", "DANGER_OCTOKIT_VERIFY_SSL"]
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize(ci_source, environment)
|
28
|
+
self.ci_source = ci_source
|
29
|
+
self.environment = environment
|
30
|
+
self.support_tokenless_auth = false
|
31
|
+
self.dismiss_out_of_range_messages = false
|
32
|
+
|
33
|
+
@token = @environment["DANGER_GITHUB_API_TOKEN"]
|
34
|
+
end
|
35
|
+
|
36
|
+
def get_pr_from_branch(repo_name, branch_name, owner)
|
37
|
+
prs = client.pull_requests(repo_name, head: "#{owner}:#{branch_name}")
|
38
|
+
unless prs.empty?
|
39
|
+
prs.first.number
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def validates_as_ci?
|
44
|
+
true
|
45
|
+
end
|
46
|
+
|
47
|
+
def validates_as_api_source?
|
48
|
+
(@token && !@token.empty?) || self.environment["DANGER_USE_LOCAL_GIT"]
|
49
|
+
end
|
50
|
+
|
51
|
+
def scm
|
52
|
+
@scm ||= GitRepo.new
|
53
|
+
end
|
54
|
+
|
55
|
+
def host
|
56
|
+
@host = @environment["DANGER_GITHUB_HOST"] || "github.com"
|
57
|
+
end
|
58
|
+
|
59
|
+
def verify_ssl
|
60
|
+
@environment["DANGER_OCTOKIT_VERIFY_SSL"] == "false" ? false : true
|
61
|
+
end
|
62
|
+
|
63
|
+
# `DANGER_GITHUB_API_HOST` is the old name kept for legacy reasons and
|
64
|
+
# backwards compatibility. `DANGER_GITHUB_API_BASE_URL` is the new
|
65
|
+
# correctly named variable.
|
66
|
+
def api_url
|
67
|
+
@environment.fetch("DANGER_GITHUB_API_HOST") do
|
68
|
+
@environment.fetch("DANGER_GITHUB_API_BASE_URL") do
|
69
|
+
"https://api.github.com/".freeze
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def client
|
75
|
+
raise "No API token given, please provide one using `DANGER_GITHUB_API_TOKEN`" if !@token && !support_tokenless_auth
|
76
|
+
@client ||= begin
|
77
|
+
Octokit.configure do |config|
|
78
|
+
config.connection_options[:ssl] = { verify: verify_ssl }
|
79
|
+
end
|
80
|
+
Octokit::Client.new(access_token: @token, auto_paginate: true, api_endpoint: api_url)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def pr_diff
|
85
|
+
@pr_diff ||= client.pull_request(ci_source.repo_slug, ci_source.pull_request_id, accept: "application/vnd.github.v3.diff")
|
86
|
+
end
|
87
|
+
|
88
|
+
def review
|
89
|
+
return @review unless @review.nil?
|
90
|
+
begin
|
91
|
+
@review = client.pull_request_reviews(ci_source.repo_slug, ci_source.pull_request_id)
|
92
|
+
.map { |review_json| Danger::RequestSources::GitHubSource::Review.new(client, ci_source, review_json) }
|
93
|
+
.select(&:generated_by_danger?)
|
94
|
+
.last
|
95
|
+
@review ||= Danger::RequestSources::GitHubSource::Review.new(client, ci_source)
|
96
|
+
@review
|
97
|
+
rescue Octokit::NotFound
|
98
|
+
@review = Danger::RequestSources::GitHubSource::ReviewUnsupported.new
|
99
|
+
@review
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def setup_danger_branches
|
104
|
+
# we can use a github specific feature here:
|
105
|
+
base_branch = self.pr_json["base"]["ref"]
|
106
|
+
base_commit = self.pr_json["base"]["sha"]
|
107
|
+
head_branch = self.pr_json["head"]["ref"]
|
108
|
+
head_commit = self.pr_json["head"]["sha"]
|
109
|
+
|
110
|
+
# Next, we want to ensure that we have a version of the current branch at a known location
|
111
|
+
scm.ensure_commitish_exists_on_branch! base_branch, base_commit
|
112
|
+
self.scm.exec "branch #{EnvironmentManager.danger_base_branch} #{base_commit}"
|
113
|
+
|
114
|
+
# OK, so we want to ensure that we have a known head branch, this will always represent
|
115
|
+
# the head of the PR ( e.g. the most recent commit that will be merged. )
|
116
|
+
scm.ensure_commitish_exists_on_branch! head_branch, head_commit
|
117
|
+
self.scm.exec "branch #{EnvironmentManager.danger_head_branch} #{head_commit}"
|
118
|
+
end
|
119
|
+
|
120
|
+
def fetch_details
|
121
|
+
self.pr_json = client.pull_request(ci_source.repo_slug, ci_source.pull_request_id)
|
122
|
+
if self.pr_json["message"] == "Moved Permanently"
|
123
|
+
raise "Repo moved or renamed, make sure to update the git remote".red
|
124
|
+
end
|
125
|
+
|
126
|
+
fetch_issue_details(self.pr_json)
|
127
|
+
self.ignored_violations = ignored_violations_from_pr
|
128
|
+
end
|
129
|
+
|
130
|
+
def ignored_violations_from_pr
|
131
|
+
GetIgnoredViolation.new(self.pr_json["body"]).call
|
132
|
+
end
|
133
|
+
|
134
|
+
def fetch_issue_details(pr_json)
|
135
|
+
href = pr_json["_links"]["issue"]["href"]
|
136
|
+
self.issue_json = client.get(href)
|
137
|
+
end
|
138
|
+
|
139
|
+
def issue_comments
|
140
|
+
@comments ||= begin
|
141
|
+
client.issue_comments(ci_source.repo_slug, ci_source.pull_request_id)
|
142
|
+
.map { |comment| Comment.from_github(comment) }
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Sending data to GitHub
|
147
|
+
def update_pull_request!(warnings: [], errors: [], messages: [], markdowns: [], danger_id: "danger", new_comment: false, remove_previous_comments: false)
|
148
|
+
comment_result = {}
|
149
|
+
editable_comments = issue_comments.select { |comment| comment.generated_by_danger?(danger_id) }
|
150
|
+
last_comment = editable_comments.last
|
151
|
+
should_create_new_comment = new_comment || last_comment.nil? || remove_previous_comments
|
152
|
+
|
153
|
+
previous_violations =
|
154
|
+
if should_create_new_comment
|
155
|
+
{}
|
156
|
+
else
|
157
|
+
parse_comment(last_comment.body)
|
158
|
+
end
|
159
|
+
|
160
|
+
regular_violations = regular_violations_group(
|
161
|
+
warnings: warnings,
|
162
|
+
errors: errors,
|
163
|
+
messages: messages,
|
164
|
+
markdowns: markdowns
|
165
|
+
)
|
166
|
+
|
167
|
+
inline_violations = inline_violations_group(
|
168
|
+
warnings: warnings,
|
169
|
+
errors: errors,
|
170
|
+
messages: messages,
|
171
|
+
markdowns: markdowns
|
172
|
+
)
|
173
|
+
|
174
|
+
rest_inline_violations = submit_inline_comments!({
|
175
|
+
danger_id: danger_id,
|
176
|
+
previous_violations: previous_violations
|
177
|
+
}.merge(**inline_violations))
|
178
|
+
|
179
|
+
main_violations = merge_violations(
|
180
|
+
regular_violations, rest_inline_violations
|
181
|
+
)
|
182
|
+
|
183
|
+
main_violations_sum = main_violations.values.inject(:+)
|
184
|
+
|
185
|
+
if (previous_violations.empty? && main_violations_sum.empty?) || remove_previous_comments
|
186
|
+
# Just remove the comment, if there's nothing to say or --remove-previous-comments CLI was set.
|
187
|
+
delete_old_comments!(danger_id: danger_id)
|
188
|
+
end
|
189
|
+
|
190
|
+
# If there are still violations to show
|
191
|
+
if main_violations_sum.any?
|
192
|
+
body = generate_comment({
|
193
|
+
template: "github",
|
194
|
+
danger_id: danger_id,
|
195
|
+
previous_violations: previous_violations
|
196
|
+
}.merge(**main_violations))
|
197
|
+
|
198
|
+
comment_result =
|
199
|
+
if should_create_new_comment
|
200
|
+
client.add_comment(ci_source.repo_slug, ci_source.pull_request_id, body)
|
201
|
+
else
|
202
|
+
client.update_comment(ci_source.repo_slug, last_comment.id, body)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
# Now, set the pull request status.
|
207
|
+
# Note: this can terminate the entire process.
|
208
|
+
submit_pull_request_status!(
|
209
|
+
warnings: warnings,
|
210
|
+
errors: errors,
|
211
|
+
details_url: comment_result["html_url"],
|
212
|
+
danger_id: danger_id
|
213
|
+
)
|
214
|
+
end
|
215
|
+
|
216
|
+
def submit_pull_request_status!(warnings: [], errors: [], details_url: [], danger_id: "danger")
|
217
|
+
status = (errors.count.zero? ? "success" : "failure")
|
218
|
+
message = generate_description(warnings: warnings, errors: errors)
|
219
|
+
latest_pr_commit_ref = self.pr_json["head"]["sha"]
|
220
|
+
|
221
|
+
if latest_pr_commit_ref.empty? || latest_pr_commit_ref.nil?
|
222
|
+
raise "Couldn't find a commit to update its status".red
|
223
|
+
end
|
224
|
+
|
225
|
+
begin
|
226
|
+
client.create_status(ci_source.repo_slug, latest_pr_commit_ref, status, {
|
227
|
+
description: message,
|
228
|
+
context: "danger/#{danger_id}",
|
229
|
+
target_url: details_url
|
230
|
+
})
|
231
|
+
rescue
|
232
|
+
# This usually means the user has no commit access to this repo
|
233
|
+
# That's always the case for open source projects where you can only
|
234
|
+
# use a read-only GitHub account
|
235
|
+
if errors.count > 0
|
236
|
+
# We need to fail the actual build here
|
237
|
+
is_private = pr_json["base"]["repo"]["private"]
|
238
|
+
if is_private
|
239
|
+
abort("\nDanger has failed this build. \nFound #{'error'.danger_pluralize(errors.count)} and I don't have write access to the PR to set a PR status.")
|
240
|
+
else
|
241
|
+
abort("\nDanger has failed this build. \nFound #{'error'.danger_pluralize(errors.count)}.")
|
242
|
+
end
|
243
|
+
else
|
244
|
+
puts message
|
245
|
+
puts "\nDanger does not have write access to the PR to set a PR status.".yellow
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
# Get rid of the previously posted comment, to only have the latest one
|
251
|
+
def delete_old_comments!(except: nil, danger_id: "danger")
|
252
|
+
issue_comments.each do |comment|
|
253
|
+
next unless comment.generated_by_danger?(danger_id)
|
254
|
+
next if comment.id == except
|
255
|
+
client.delete_comment(ci_source.repo_slug, comment.id)
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
def submit_inline_comments!(warnings: [], errors: [], messages: [], markdowns: [], previous_violations: [], danger_id: "danger")
|
260
|
+
# Avoid doing any fetchs if there's no inline comments
|
261
|
+
return {} if (warnings + errors + messages + markdowns).select(&:inline?).empty?
|
262
|
+
|
263
|
+
diff_lines = self.pr_diff.lines
|
264
|
+
pr_comments = client.pull_request_comments(ci_source.repo_slug, ci_source.pull_request_id)
|
265
|
+
danger_comments = pr_comments.select { |comment| Comment.from_github(comment).generated_by_danger?(danger_id) }
|
266
|
+
non_danger_comments = pr_comments - danger_comments
|
267
|
+
|
268
|
+
warnings = submit_inline_comments_for_kind!(:warning, warnings, diff_lines, danger_comments, previous_violations["warning"], danger_id: danger_id)
|
269
|
+
errors = submit_inline_comments_for_kind!(:error, errors, diff_lines, danger_comments, previous_violations["error"], danger_id: danger_id)
|
270
|
+
messages = submit_inline_comments_for_kind!(:message, messages, diff_lines, danger_comments, previous_violations["message"], danger_id: danger_id)
|
271
|
+
markdowns = submit_inline_comments_for_kind!(:markdown, markdowns, diff_lines, danger_comments, [], danger_id: danger_id)
|
272
|
+
|
273
|
+
# submit removes from the array all comments that are still in force
|
274
|
+
# so we strike out all remaining ones
|
275
|
+
danger_comments.each do |comment|
|
276
|
+
violation = violations_from_table(comment["body"]).first
|
277
|
+
if !violation.nil? && violation.sticky
|
278
|
+
body = generate_inline_comment_body("white_check_mark", violation, danger_id: danger_id, resolved: true, template: "github")
|
279
|
+
client.update_pull_request_comment(ci_source.repo_slug, comment["id"], body)
|
280
|
+
else
|
281
|
+
# We remove non-sticky violations that have no replies
|
282
|
+
# Since there's no direct concept of a reply in GH, we simply consider
|
283
|
+
# the existence of non-danger comments in that line as replies
|
284
|
+
replies = non_danger_comments.select do |potential|
|
285
|
+
potential["path"] == comment["path"] &&
|
286
|
+
potential["position"] == comment["position"] &&
|
287
|
+
potential["commit_id"] == comment["commit_id"]
|
288
|
+
end
|
289
|
+
|
290
|
+
client.delete_pull_request_comment(ci_source.repo_slug, comment["id"]) if replies.empty?
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
{
|
295
|
+
warnings: warnings,
|
296
|
+
errors: errors,
|
297
|
+
messages: messages,
|
298
|
+
markdowns: markdowns
|
299
|
+
}
|
300
|
+
end
|
301
|
+
|
302
|
+
def messages_are_equivalent(m1, m2)
|
303
|
+
blob_regexp = %r{blob/[0-9a-z]+/}
|
304
|
+
m1.file == m2.file && m1.line == m2.line &&
|
305
|
+
m1.message.sub(blob_regexp, "") == m2.message.sub(blob_regexp, "")
|
306
|
+
end
|
307
|
+
|
308
|
+
def submit_inline_comments_for_kind!(kind, messages, diff_lines, danger_comments, previous_violations, danger_id: "danger")
|
309
|
+
head_ref = pr_json["head"]["sha"]
|
310
|
+
previous_violations ||= []
|
311
|
+
is_markdown_content = kind == :markdown
|
312
|
+
emoji = { warning: "warning", error: "no_entry_sign", message: "book" }[kind]
|
313
|
+
|
314
|
+
messages.reject do |m|
|
315
|
+
next false unless m.file && m.line
|
316
|
+
|
317
|
+
position = find_position_in_diff diff_lines, m, kind
|
318
|
+
|
319
|
+
# Keep the change if it's line is not in the diff and not in dismiss mode
|
320
|
+
next dismiss_out_of_range_messages_for(kind) if position.nil?
|
321
|
+
|
322
|
+
# Once we know we're gonna submit it, we format it
|
323
|
+
if is_markdown_content
|
324
|
+
body = generate_inline_markdown_body(m, danger_id: danger_id, template: "github")
|
325
|
+
else
|
326
|
+
# Hide the inline link behind a span
|
327
|
+
m = process_markdown(m, true)
|
328
|
+
body = generate_inline_comment_body(emoji, m, danger_id: danger_id, template: "github")
|
329
|
+
# A comment might be in previous_violations because only now it's part of the unified diff
|
330
|
+
# We remove from the array since it won't have a place in the table anymore
|
331
|
+
previous_violations.reject! { |v| messages_are_equivalent(v, m) }
|
332
|
+
end
|
333
|
+
|
334
|
+
matching_comments = danger_comments.select do |comment_data|
|
335
|
+
if comment_data["path"] == m.file && comment_data["position"] == position
|
336
|
+
# Parse it to avoid problems with strikethrough
|
337
|
+
violation = violations_from_table(comment_data["body"]).first
|
338
|
+
if violation
|
339
|
+
messages_are_equivalent(violation, m)
|
340
|
+
else
|
341
|
+
blob_regexp = %r{blob/[0-9a-z]+/}
|
342
|
+
comment_data["body"].sub(blob_regexp, "") == body.sub(blob_regexp, "")
|
343
|
+
end
|
344
|
+
else
|
345
|
+
false
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
if matching_comments.empty?
|
350
|
+
begin
|
351
|
+
client.create_pull_request_comment(ci_source.repo_slug, ci_source.pull_request_id,
|
352
|
+
body, head_ref, m.file, position)
|
353
|
+
rescue Octokit::UnprocessableEntity => e
|
354
|
+
# Show more detail for UnprocessableEntity error
|
355
|
+
message = [e, "body: #{body}", "head_ref: #{head_ref}", "filename: #{m.file}", "position: #{position}"].join("\n")
|
356
|
+
puts message
|
357
|
+
|
358
|
+
# Not reject because this comment has not completed
|
359
|
+
next false
|
360
|
+
end
|
361
|
+
else
|
362
|
+
# Remove the surviving comment so we don't strike it out
|
363
|
+
danger_comments.reject! { |c| matching_comments.include? c }
|
364
|
+
|
365
|
+
# Update the comment to remove the strikethrough if present
|
366
|
+
comment = matching_comments.first
|
367
|
+
client.update_pull_request_comment(ci_source.repo_slug, comment["id"], body)
|
368
|
+
end
|
369
|
+
|
370
|
+
# Remove this element from the array
|
371
|
+
next true
|
372
|
+
end
|
373
|
+
end
|
374
|
+
|
375
|
+
def find_position_in_diff(diff_lines, message, kind)
|
376
|
+
range_header_regexp = /@@ -([0-9]+)(,([0-9]+))? \+(?<start>[0-9]+)(,(?<end>[0-9]+))? @@.*/
|
377
|
+
file_header_regexp = %r{^diff --git a/.*}
|
378
|
+
|
379
|
+
pattern = "+++ b/" + message.file + "\n"
|
380
|
+
file_start = diff_lines.index(pattern)
|
381
|
+
|
382
|
+
# Files containing spaces sometimes have a trailing tab
|
383
|
+
if file_start.nil?
|
384
|
+
pattern = "+++ b/" + message.file + "\t\n"
|
385
|
+
file_start = diff_lines.index(pattern)
|
386
|
+
end
|
387
|
+
|
388
|
+
return nil if file_start.nil?
|
389
|
+
|
390
|
+
position = -1
|
391
|
+
file_line = nil
|
392
|
+
|
393
|
+
diff_lines.drop(file_start).each do |line|
|
394
|
+
# If the line has `No newline` annotation, position need increment
|
395
|
+
if line.eql?("\\n")
|
396
|
+
position += 1
|
397
|
+
next
|
398
|
+
end
|
399
|
+
# If we found the start of another file diff, we went too far
|
400
|
+
break if line.match file_header_regexp
|
401
|
+
|
402
|
+
match = line.match range_header_regexp
|
403
|
+
|
404
|
+
# file_line is set once we find the hunk the line is in
|
405
|
+
# we need to count how many lines in new file we have
|
406
|
+
# so we do it one by one ignoring the deleted lines
|
407
|
+
if !file_line.nil? && !line.start_with?("-")
|
408
|
+
if file_line == message.line
|
409
|
+
file_line = nil if dismiss_out_of_range_messages_for(kind) && !line.start_with?("+")
|
410
|
+
break
|
411
|
+
end
|
412
|
+
file_line += 1
|
413
|
+
end
|
414
|
+
|
415
|
+
# We need to count how many diff lines are between us and
|
416
|
+
# the line we're looking for
|
417
|
+
position += 1
|
418
|
+
|
419
|
+
next unless match
|
420
|
+
|
421
|
+
range_start = match[:start].to_i
|
422
|
+
if match[:end]
|
423
|
+
range_end = match[:end].to_i + range_start
|
424
|
+
else
|
425
|
+
range_end = range_start
|
426
|
+
end
|
427
|
+
|
428
|
+
# We are past the line position, just abort
|
429
|
+
break if message.line.to_i < range_start
|
430
|
+
next unless message.line.to_i >= range_start && message.line.to_i < range_end
|
431
|
+
|
432
|
+
file_line = range_start
|
433
|
+
end
|
434
|
+
|
435
|
+
position unless file_line.nil?
|
436
|
+
end
|
437
|
+
|
438
|
+
# See the tests for examples of data coming in looks like
|
439
|
+
def parse_message_from_row(row)
|
440
|
+
message_regexp = %r{(<(a |span data-)href="https://#{host}/#{ci_source.repo_slug}/blob/[0-9a-z]+/(?<file>[^#]+)#L(?<line>[0-9]+)"(>[^<]*</a> - |/>))?(?<message>.*?)}im
|
441
|
+
match = message_regexp.match(row)
|
442
|
+
|
443
|
+
if match[:line]
|
444
|
+
line = match[:line].to_i
|
445
|
+
else
|
446
|
+
line = nil
|
447
|
+
end
|
448
|
+
Violation.new(row, true, match[:file], line)
|
449
|
+
end
|
450
|
+
|
451
|
+
def markdown_link_to_message(message, hide_link)
|
452
|
+
url = "https://#{host}/#{ci_source.repo_slug}/blob/#{pr_json['head']['sha']}/#{message.file}#L#{message.line}"
|
453
|
+
|
454
|
+
if hide_link
|
455
|
+
"<span data-href=\"#{url}\"/>"
|
456
|
+
else
|
457
|
+
"[#{message.file}#L#{message.line}](#{url}) - "
|
458
|
+
end
|
459
|
+
end
|
460
|
+
|
461
|
+
# @return [String] The organisation name, is nil if it can't be detected
|
462
|
+
def organisation
|
463
|
+
matched = self.issue_json["repository_url"].match(%r{repos\/(.*)\/})
|
464
|
+
return matched[1] if matched && matched[1]
|
465
|
+
rescue
|
466
|
+
nil
|
467
|
+
end
|
468
|
+
|
469
|
+
def dismiss_out_of_range_messages_for(kind)
|
470
|
+
if self.dismiss_out_of_range_messages.kind_of?(Hash) && self.dismiss_out_of_range_messages[kind]
|
471
|
+
self.dismiss_out_of_range_messages[kind]
|
472
|
+
elsif self.dismiss_out_of_range_messages == true
|
473
|
+
self.dismiss_out_of_range_messages
|
474
|
+
else
|
475
|
+
false
|
476
|
+
end
|
477
|
+
end
|
478
|
+
|
479
|
+
# @return [String] A URL to the specific file, ready to be downloaded
|
480
|
+
def file_url(organisation: nil, repository: nil, branch: nil, path: nil)
|
481
|
+
organisation ||= self.organisation
|
482
|
+
|
483
|
+
begin
|
484
|
+
# Retrieve the download URL (default branch on nil param)
|
485
|
+
contents = client.contents("#{organisation}/#{repository}", path: path, ref: branch)
|
486
|
+
@download_url = contents["download_url"]
|
487
|
+
rescue Octokit::ClientError
|
488
|
+
# Fallback to github.com
|
489
|
+
branch ||= "master"
|
490
|
+
@download_url = "https://raw.githubusercontent.com/#{organisation}/#{repository}/#{branch}/#{path}"
|
491
|
+
end
|
492
|
+
end
|
493
|
+
|
494
|
+
private
|
495
|
+
|
496
|
+
def regular_violations_group(warnings: [], errors: [], messages: [], markdowns: [])
|
497
|
+
{
|
498
|
+
warnings: warnings.reject(&:inline?),
|
499
|
+
errors: errors.reject(&:inline?),
|
500
|
+
messages: messages.reject(&:inline?),
|
501
|
+
markdowns: markdowns.reject(&:inline?)
|
502
|
+
}
|
503
|
+
end
|
504
|
+
|
505
|
+
def inline_violations_group(warnings: [], errors: [], messages: [], markdowns: [])
|
506
|
+
cmp = proc do |a, b|
|
507
|
+
next -1 unless a.file && a.line
|
508
|
+
next 1 unless b.file && b.line
|
509
|
+
|
510
|
+
next a.line <=> b.line if a.file == b.file
|
511
|
+
next a.file <=> b.file
|
512
|
+
end
|
513
|
+
|
514
|
+
# Sort to group inline comments by file
|
515
|
+
{
|
516
|
+
warnings: warnings.select(&:inline?).sort(&cmp),
|
517
|
+
errors: errors.select(&:inline?).sort(&cmp),
|
518
|
+
messages: messages.select(&:inline?).sort(&cmp),
|
519
|
+
markdowns: markdowns.select(&:inline?).sort(&cmp)
|
520
|
+
}
|
521
|
+
end
|
522
|
+
|
523
|
+
def merge_violations(*violation_groups)
|
524
|
+
violation_groups.inject({}) do |accumulator, group|
|
525
|
+
accumulator.merge(group) { |_, old, fresh| old + fresh }
|
526
|
+
end
|
527
|
+
end
|
528
|
+
end
|
529
|
+
end
|
530
|
+
end
|