danger 8.0.4

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 (121) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +22 -0
  3. data/README.md +94 -0
  4. data/bin/danger +5 -0
  5. data/lib/assets/DangerfileTemplate +13 -0
  6. data/lib/danger.rb +44 -0
  7. data/lib/danger/ci_source/appcenter.rb +55 -0
  8. data/lib/danger/ci_source/appveyor.rb +60 -0
  9. data/lib/danger/ci_source/azure_pipelines.rb +44 -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 +65 -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 +57 -0
  20. data/lib/danger/ci_source/codefresh.rb +53 -0
  21. data/lib/danger/ci_source/codeship.rb +44 -0
  22. data/lib/danger/ci_source/dotci.rb +52 -0
  23. data/lib/danger/ci_source/drone.rb +71 -0
  24. data/lib/danger/ci_source/github_actions.rb +43 -0
  25. data/lib/danger/ci_source/gitlab_ci.rb +86 -0
  26. data/lib/danger/ci_source/jenkins.rb +149 -0
  27. data/lib/danger/ci_source/local_git_repo.rb +119 -0
  28. data/lib/danger/ci_source/local_only_git_repo.rb +47 -0
  29. data/lib/danger/ci_source/screwdriver.rb +47 -0
  30. data/lib/danger/ci_source/semaphore.rb +37 -0
  31. data/lib/danger/ci_source/support/commits.rb +17 -0
  32. data/lib/danger/ci_source/support/find_repo_info_from_logs.rb +35 -0
  33. data/lib/danger/ci_source/support/find_repo_info_from_url.rb +42 -0
  34. data/lib/danger/ci_source/support/local_pull_request.rb +14 -0
  35. data/lib/danger/ci_source/support/no_pull_request.rb +7 -0
  36. data/lib/danger/ci_source/support/no_repo_info.rb +5 -0
  37. data/lib/danger/ci_source/support/pull_request_finder.rb +179 -0
  38. data/lib/danger/ci_source/support/remote_pull_request.rb +15 -0
  39. data/lib/danger/ci_source/support/repo_info.rb +10 -0
  40. data/lib/danger/ci_source/surf.rb +37 -0
  41. data/lib/danger/ci_source/teamcity.rb +161 -0
  42. data/lib/danger/ci_source/travis.rb +51 -0
  43. data/lib/danger/ci_source/vsts.rb +73 -0
  44. data/lib/danger/ci_source/xcode_server.rb +48 -0
  45. data/lib/danger/clients/rubygems_client.rb +14 -0
  46. data/lib/danger/commands/dangerfile/gem.rb +43 -0
  47. data/lib/danger/commands/dangerfile/init.rb +30 -0
  48. data/lib/danger/commands/dry_run.rb +54 -0
  49. data/lib/danger/commands/init.rb +297 -0
  50. data/lib/danger/commands/init_helpers/interviewer.rb +92 -0
  51. data/lib/danger/commands/local.rb +83 -0
  52. data/lib/danger/commands/local_helpers/http_cache.rb +36 -0
  53. data/lib/danger/commands/local_helpers/local_setup.rb +46 -0
  54. data/lib/danger/commands/local_helpers/pry_setup.rb +31 -0
  55. data/lib/danger/commands/plugins/plugin_json.rb +46 -0
  56. data/lib/danger/commands/plugins/plugin_lint.rb +54 -0
  57. data/lib/danger/commands/plugins/plugin_readme.rb +45 -0
  58. data/lib/danger/commands/pr.rb +92 -0
  59. data/lib/danger/commands/runner.rb +94 -0
  60. data/lib/danger/commands/staging.rb +53 -0
  61. data/lib/danger/commands/systems.rb +43 -0
  62. data/lib/danger/comment_generators/bitbucket_server.md.erb +20 -0
  63. data/lib/danger/comment_generators/bitbucket_server_inline.md.erb +15 -0
  64. data/lib/danger/comment_generators/bitbucket_server_message_group.md.erb +12 -0
  65. data/lib/danger/comment_generators/github.md.erb +55 -0
  66. data/lib/danger/comment_generators/github_inline.md.erb +26 -0
  67. data/lib/danger/comment_generators/gitlab.md.erb +40 -0
  68. data/lib/danger/comment_generators/gitlab_inline.md.erb +26 -0
  69. data/lib/danger/comment_generators/vsts.md.erb +20 -0
  70. data/lib/danger/core_ext/file_list.rb +18 -0
  71. data/lib/danger/core_ext/string.rb +20 -0
  72. data/lib/danger/danger_core/dangerfile.rb +341 -0
  73. data/lib/danger/danger_core/dangerfile_dsl.rb +29 -0
  74. data/lib/danger/danger_core/dangerfile_generator.rb +11 -0
  75. data/lib/danger/danger_core/environment_manager.rb +123 -0
  76. data/lib/danger/danger_core/executor.rb +92 -0
  77. data/lib/danger/danger_core/message_aggregator.rb +49 -0
  78. data/lib/danger/danger_core/message_group.rb +68 -0
  79. data/lib/danger/danger_core/messages/base.rb +56 -0
  80. data/lib/danger/danger_core/messages/markdown.rb +42 -0
  81. data/lib/danger/danger_core/messages/violation.rb +54 -0
  82. data/lib/danger/danger_core/plugins/dangerfile_bitbucket_cloud_plugin.rb +144 -0
  83. data/lib/danger/danger_core/plugins/dangerfile_bitbucket_server_plugin.rb +211 -0
  84. data/lib/danger/danger_core/plugins/dangerfile_danger_plugin.rb +248 -0
  85. data/lib/danger/danger_core/plugins/dangerfile_git_plugin.rb +158 -0
  86. data/lib/danger/danger_core/plugins/dangerfile_github_plugin.rb +254 -0
  87. data/lib/danger/danger_core/plugins/dangerfile_gitlab_plugin.rb +240 -0
  88. data/lib/danger/danger_core/plugins/dangerfile_local_only_plugin.rb +42 -0
  89. data/lib/danger/danger_core/plugins/dangerfile_messaging_plugin.rb +218 -0
  90. data/lib/danger/danger_core/plugins/dangerfile_vsts_plugin.rb +191 -0
  91. data/lib/danger/danger_core/standard_error.rb +143 -0
  92. data/lib/danger/helpers/array_subclass.rb +61 -0
  93. data/lib/danger/helpers/comment.rb +32 -0
  94. data/lib/danger/helpers/comments_helper.rb +178 -0
  95. data/lib/danger/helpers/comments_parsing_helper.rb +70 -0
  96. data/lib/danger/helpers/emoji_mapper.rb +41 -0
  97. data/lib/danger/helpers/find_max_num_violations.rb +31 -0
  98. data/lib/danger/helpers/message_groups_array_helper.rb +31 -0
  99. data/lib/danger/plugin_support/gems_resolver.rb +77 -0
  100. data/lib/danger/plugin_support/plugin.rb +49 -0
  101. data/lib/danger/plugin_support/plugin_file_resolver.rb +30 -0
  102. data/lib/danger/plugin_support/plugin_linter.rb +161 -0
  103. data/lib/danger/plugin_support/plugin_parser.rb +199 -0
  104. data/lib/danger/plugin_support/templates/readme_table.html.erb +26 -0
  105. data/lib/danger/request_sources/bitbucket_cloud.rb +171 -0
  106. data/lib/danger/request_sources/bitbucket_cloud_api.rb +181 -0
  107. data/lib/danger/request_sources/bitbucket_server.rb +105 -0
  108. data/lib/danger/request_sources/bitbucket_server_api.rb +117 -0
  109. data/lib/danger/request_sources/github/github.rb +530 -0
  110. data/lib/danger/request_sources/github/github_review.rb +126 -0
  111. data/lib/danger/request_sources/github/github_review_resolver.rb +19 -0
  112. data/lib/danger/request_sources/github/github_review_unsupported.rb +25 -0
  113. data/lib/danger/request_sources/gitlab.rb +525 -0
  114. data/lib/danger/request_sources/local_only.rb +53 -0
  115. data/lib/danger/request_sources/request_source.rb +85 -0
  116. data/lib/danger/request_sources/support/get_ignored_violation.rb +17 -0
  117. data/lib/danger/request_sources/vsts.rb +118 -0
  118. data/lib/danger/request_sources/vsts_api.rb +138 -0
  119. data/lib/danger/scm_source/git_repo.rb +181 -0
  120. data/lib/danger/version.rb +4 -0
  121. 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