danger-additional-logging 0.0.1

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