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,557 @@
1
+ require "uri"
2
+ require "danger/helpers/comments_helper"
3
+ require "danger/helpers/comment"
4
+ require "danger/request_sources/support/get_ignored_violation"
5
+
6
+ module Danger
7
+ module RequestSources
8
+ class GitLab < RequestSource
9
+ include Danger::Helpers::CommentsHelper
10
+ attr_accessor :mr_json, :commits_json, :dismiss_out_of_range_messages, :endpoint, :host
11
+
12
+ FIRST_GITLAB_GEM_WITH_VERSION_CHECK = Gem::Version.new("4.6.0")
13
+ FIRST_VERSION_WITH_INLINE_COMMENTS = Gem::Version.new("10.8.0")
14
+
15
+ def self.env_vars
16
+ ["DANGER_GITLAB_API_TOKEN"]
17
+ end
18
+
19
+ def self.optional_env_vars
20
+ ["DANGER_GITLAB_HOST", "DANGER_GITLAB_API_BASE_URL"]
21
+ end
22
+
23
+ def initialize(ci_source, environment)
24
+ self.ci_source = ci_source
25
+ self.dismiss_out_of_range_messages = false
26
+ @endpoint = environment["DANGER_GITLAB_API_BASE_URL"] || environment.fetch("CI_API_V4_URL", "https://gitlab.com/api/v4")
27
+ @host = environment.fetch("DANGER_GITLAB_HOST", URI.parse(endpoint).host) || "gitlab.com"
28
+ @token = environment["DANGER_GITLAB_API_TOKEN"]
29
+ end
30
+
31
+ def client
32
+ raise "No API token given, please provide one using `DANGER_GITLAB_API_TOKEN`" unless @token
33
+
34
+ # The require happens inline so that it won't cause exceptions when just using the `danger` gem.
35
+ require "gitlab"
36
+
37
+ @client ||= Gitlab.client(endpoint: endpoint, private_token: @token)
38
+ rescue LoadError => e
39
+ if e.path == "gitlab"
40
+ puts "The GitLab gem was not installed, you will need to change your Gem from `danger` to `danger-gitlab`.".red
41
+ puts "\n - See https://github.com/danger/danger/blob/master/CHANGELOG.md#400"
42
+ else
43
+ puts "Error: #{e}".red
44
+ end
45
+ abort
46
+ end
47
+
48
+ def validates_as_ci?
49
+ includes_port = host.include? ":"
50
+ raise "Port number included in `DANGER_GITLAB_HOST`, this will fail with GitLab CI Runners" if includes_port
51
+
52
+ # We don't call super because in some cases the Git remote doesn't match the GitLab instance host.
53
+ # In Danger::EnvironmentManager#initialize we still check that the request source is #validates_as_api_source?
54
+ # so that should be sufficient to validate GitLab as request source.
55
+ # See https://github.com/danger/danger/issues/1231 and https://gitlab.com/gitlab-com/gl-infra/infrastructure/-/issues/10069.
56
+ true
57
+ end
58
+
59
+ def validates_as_api_source?
60
+ @token && !@token.empty?
61
+ end
62
+
63
+ def scm
64
+ @scm ||= GitRepo.new
65
+ end
66
+
67
+ def base_commit
68
+ @base_commit ||= self.mr_json.diff_refs.base_sha
69
+ end
70
+
71
+ def mr_comments
72
+ # @raw_comments contains what we got back from the server.
73
+ # @comments contains Comment objects (that have less information)
74
+ @comments ||= if supports_inline_comments
75
+ @raw_comments = mr_discussions
76
+ .auto_paginate
77
+ .flat_map { |discussion| discussion.notes.map { |note| note.to_h.merge({ "discussion_id" => discussion.id }) } }
78
+ @raw_comments
79
+ .map { |comment| Comment.from_gitlab(comment) }
80
+ else
81
+ @raw_comments = client.merge_request_comments(ci_source.repo_slug, ci_source.pull_request_id, per_page: 100)
82
+ .auto_paginate
83
+ @raw_comments
84
+ .map { |comment| Comment.from_gitlab(comment) }
85
+ end
86
+ end
87
+
88
+ def mr_discussions
89
+ @mr_discussions ||= client.merge_request_discussions(ci_source.repo_slug, ci_source.pull_request_id)
90
+ end
91
+
92
+ def mr_diff
93
+ @mr_diff ||= begin
94
+ diffs = mr_changes.changes.map do |change|
95
+ diff = change["diff"]
96
+ if diff.start_with?("--- a/")
97
+ diff
98
+ else
99
+ "--- a/#{change['old_path']}\n+++ b/#{change['new_path']}\n#{diff}"
100
+ end
101
+ end
102
+ diffs.join("\n")
103
+ end
104
+ end
105
+
106
+ def mr_changed_paths
107
+ @mr_changed_paths ||= mr_changes
108
+ .changes.map { |change| change["new_path"] }
109
+
110
+ @mr_changed_paths
111
+ end
112
+
113
+ def mr_changes
114
+ @mr_changes ||= client.merge_request_changes(ci_source.repo_slug, ci_source.pull_request_id)
115
+ end
116
+
117
+ def mr_closes_issues
118
+ @mr_closes_issues ||= client.merge_request_closes_issues(ci_source.repo_slug, ci_source.pull_request_id)
119
+ end
120
+
121
+ def setup_danger_branches
122
+ # we can use a GitLab specific feature here:
123
+ base_branch = self.mr_json.source_branch
124
+ base_commit = self.mr_json.diff_refs.base_sha
125
+ head_branch = self.mr_json.target_branch
126
+ head_commit = self.mr_json.diff_refs.head_sha
127
+
128
+ # Next, we want to ensure that we have a version of the current branch at a known location
129
+ scm.ensure_commitish_exists_on_branch! base_branch, base_commit
130
+ self.scm.exec "branch #{EnvironmentManager.danger_base_branch} #{base_commit}"
131
+
132
+ # OK, so we want to ensure that we have a known head branch, this will always represent
133
+ # the head of the PR ( e.g. the most recent commit that will be merged. )
134
+ scm.ensure_commitish_exists_on_branch! head_branch, head_commit
135
+ self.scm.exec "branch #{EnvironmentManager.danger_head_branch} #{head_commit}"
136
+ end
137
+
138
+ def fetch_details
139
+ self.mr_json = client.merge_request(ci_source.repo_slug, self.ci_source.pull_request_id)
140
+ self.ignored_violations = ignored_violations_from_pr
141
+ end
142
+
143
+ def ignored_violations_from_pr
144
+ GetIgnoredViolation.new(self.mr_json.description).call
145
+ end
146
+
147
+ def supports_inline_comments
148
+ # If we can't check GitLab's version, we assume we don't support inline comments
149
+ @supports_inline_comments ||= if Gem.loaded_specs["gitlab"].version < FIRST_GITLAB_GEM_WITH_VERSION_CHECK
150
+ false
151
+ else
152
+ current_version = Gem::Version.new(client.version.version)
153
+
154
+ current_version >= FIRST_VERSION_WITH_INLINE_COMMENTS
155
+ end
156
+ end
157
+
158
+ def update_pull_request!(warnings: [], errors: [], messages: [], markdowns: [], danger_id: "danger", new_comment: false, remove_previous_comments: false)
159
+ if supports_inline_comments
160
+ update_pull_request_with_inline_comments!(warnings: warnings, errors: errors, messages: messages, markdowns: markdowns, danger_id: danger_id, new_comment: new_comment, remove_previous_comments: remove_previous_comments)
161
+ else
162
+ update_pull_request_without_inline_comments!(warnings: warnings, errors: errors, messages: messages, markdowns: markdowns, danger_id: danger_id, new_comment: new_comment, remove_previous_comments: remove_previous_comments)
163
+ end
164
+ end
165
+
166
+ def update_pull_request_with_inline_comments!(warnings: [], errors: [], messages: [], markdowns: [], danger_id: "danger", new_comment: false, remove_previous_comments: false)
167
+ editable_regular_comments = mr_comments
168
+ .select { |comment| comment.generated_by_danger?(danger_id) }
169
+ .reject(&:inline?)
170
+
171
+ last_comment = editable_regular_comments.last
172
+ should_create_new_comment = new_comment || last_comment.nil? || remove_previous_comments
173
+
174
+ previous_violations =
175
+ if should_create_new_comment
176
+ {}
177
+ else
178
+ parse_comment(last_comment.body)
179
+ end
180
+
181
+ regular_violations = regular_violations_group(
182
+ warnings: warnings,
183
+ errors: errors,
184
+ messages: messages,
185
+ markdowns: markdowns
186
+ )
187
+
188
+ inline_violations = inline_violations_group(
189
+ warnings: warnings,
190
+ errors: errors,
191
+ messages: messages,
192
+ markdowns: markdowns
193
+ )
194
+
195
+ rest_inline_violations = submit_inline_comments!(**{
196
+ danger_id: danger_id,
197
+ previous_violations: previous_violations
198
+ }.merge(inline_violations))
199
+
200
+ main_violations = merge_violations(
201
+ regular_violations, rest_inline_violations
202
+ )
203
+
204
+ main_violations_sum = main_violations.values.inject(:+)
205
+
206
+ if (previous_violations.empty? && main_violations_sum.empty?) || remove_previous_comments
207
+ # Just remove the comment, if there's nothing to say or --remove-previous-comments CLI was set.
208
+ delete_old_comments!(danger_id: danger_id)
209
+ end
210
+
211
+ # If there are still violations to show
212
+ if main_violations_sum.any?
213
+ body = generate_comment(**{
214
+ template: "gitlab",
215
+ danger_id: danger_id,
216
+ previous_violations: previous_violations
217
+ }.merge(main_violations))
218
+
219
+ comment_result =
220
+ if should_create_new_comment
221
+ client.create_merge_request_note(ci_source.repo_slug, ci_source.pull_request_id, body)
222
+ else
223
+ client.edit_merge_request_note(ci_source.repo_slug, ci_source.pull_request_id, last_comment.id, body)
224
+ end
225
+ end
226
+ end
227
+
228
+ def update_pull_request_without_inline_comments!(warnings: [], errors: [], messages: [], markdowns: [], danger_id: "danger", new_comment: false, remove_previous_comments: false)
229
+ editable_comments = mr_comments.select { |comment| comment.generated_by_danger?(danger_id) }
230
+
231
+ should_create_new_comment = new_comment || editable_comments.empty? || remove_previous_comments
232
+
233
+ if should_create_new_comment
234
+ previous_violations = {}
235
+ else
236
+ comment = editable_comments.first.body
237
+ previous_violations = parse_comment(comment)
238
+ end
239
+
240
+ if (previous_violations.empty? && (warnings + errors + messages + markdowns).empty?) || remove_previous_comments
241
+ # Just remove the comment, if there's nothing to say or --remove-previous-comments CLI was set.
242
+ delete_old_comments!(danger_id: danger_id)
243
+ else
244
+ body = generate_comment(warnings: warnings,
245
+ errors: errors,
246
+ messages: messages,
247
+ markdowns: markdowns,
248
+ previous_violations: previous_violations,
249
+ danger_id: danger_id,
250
+ template: "gitlab")
251
+
252
+ if editable_comments.empty? or should_create_new_comment
253
+ client.create_merge_request_comment(
254
+ ci_source.repo_slug, ci_source.pull_request_id, body
255
+ )
256
+ else
257
+ original_id = editable_comments.first.id
258
+ client.edit_merge_request_comment(
259
+ ci_source.repo_slug,
260
+ ci_source.pull_request_id,
261
+ original_id,
262
+ { body: body }
263
+ )
264
+ end
265
+ end
266
+ end
267
+
268
+ def delete_old_comments!(except: nil, danger_id: "danger")
269
+ @raw_comments.each do |raw_comment|
270
+ comment = Comment.from_gitlab(raw_comment)
271
+ next unless comment.generated_by_danger?(danger_id)
272
+ next if comment.id == except
273
+ next unless raw_comment.kind_of?(Hash) && raw_comment["position"].nil?
274
+
275
+ begin
276
+ client.delete_merge_request_comment(
277
+ ci_source.repo_slug,
278
+ ci_source.pull_request_id,
279
+ comment.id
280
+ )
281
+ rescue StandardError
282
+ end
283
+ end
284
+ end
285
+
286
+ def markdown_link_to_message(message, _)
287
+ "#{message.file}#L#{message.line}: "
288
+ end
289
+
290
+ # @return [String] The organisation name, is nil if it can't be detected
291
+ def organisation
292
+ nil # TODO: Implement this
293
+ end
294
+
295
+ def dismiss_out_of_range_messages_for(kind)
296
+ if self.dismiss_out_of_range_messages.kind_of?(Hash) && self.dismiss_out_of_range_messages[kind]
297
+ self.dismiss_out_of_range_messages[kind]
298
+ elsif self.dismiss_out_of_range_messages == true
299
+ self.dismiss_out_of_range_messages
300
+ else
301
+ false
302
+ end
303
+ end
304
+
305
+ # @return [String] A URL to the specific file, ready to be downloaded
306
+ def file_url(organisation: nil, repository: nil, ref: nil, branch: nil, path: nil)
307
+ ref ||= (branch || "master")
308
+ # According to GitLab Repositories API docs path and id(slug) should be encoded.
309
+ path = URI.encode_www_form_component(path)
310
+ repository = URI.encode_www_form_component(repository)
311
+ "#{endpoint}/projects/#{repository}/repository/files/#{path}/raw?ref=#{ref}&private_token=#{@token}"
312
+ end
313
+
314
+ def regular_violations_group(warnings: [], errors: [], messages: [], markdowns: [])
315
+ {
316
+ warnings: warnings.reject(&:inline?),
317
+ errors: errors.reject(&:inline?),
318
+ messages: messages.reject(&:inline?),
319
+ markdowns: markdowns.reject(&:inline?)
320
+ }
321
+ end
322
+
323
+ def inline_violations_group(warnings: [], errors: [], messages: [], markdowns: [])
324
+ cmp = proc do |a, b|
325
+ next -1 unless a.file && a.line
326
+ next 1 unless b.file && b.line
327
+
328
+ next a.line <=> b.line if a.file == b.file
329
+
330
+ next a.file <=> b.file
331
+ end
332
+
333
+ # Sort to group inline comments by file
334
+ {
335
+ warnings: warnings.select(&:inline?).sort(&cmp),
336
+ errors: errors.select(&:inline?).sort(&cmp),
337
+ messages: messages.select(&:inline?).sort(&cmp),
338
+ markdowns: markdowns.select(&:inline?).sort(&cmp)
339
+ }
340
+ end
341
+
342
+ def merge_violations(*violation_groups)
343
+ violation_groups.inject({}) do |accumulator, group|
344
+ accumulator.merge(group) { |_, old, fresh| old + fresh }
345
+ end
346
+ end
347
+
348
+ def submit_inline_comments!(warnings: [], errors: [], messages: [], markdowns: [], previous_violations: [], danger_id: "danger")
349
+ comments = mr_discussions
350
+ .auto_paginate
351
+ .flat_map { |discussion| discussion.notes.map { |note| note.to_h.merge({ "discussion_id" => discussion.id }) } }
352
+ .select { |comment| Comment.from_gitlab(comment).inline? }
353
+
354
+ danger_comments = comments.select { |comment| Comment.from_gitlab(comment).generated_by_danger?(danger_id) }
355
+ non_danger_comments = comments - danger_comments
356
+
357
+ diff_lines = []
358
+
359
+ warnings = submit_inline_comments_for_kind!(:warning, warnings, diff_lines, danger_comments, previous_violations["warning"], danger_id: danger_id)
360
+ errors = submit_inline_comments_for_kind!(:error, errors, diff_lines, danger_comments, previous_violations["error"], danger_id: danger_id)
361
+ messages = submit_inline_comments_for_kind!(:message, messages, diff_lines, danger_comments, previous_violations["message"], danger_id: danger_id)
362
+ markdowns = submit_inline_comments_for_kind!(:markdown, markdowns, diff_lines, danger_comments, [], danger_id: danger_id)
363
+
364
+ # submit removes from the array all comments that are still in force
365
+ # so we strike out all remaining ones
366
+ danger_comments.each do |comment|
367
+ violation = violations_from_table(comment["body"]).first
368
+ if !violation.nil? && violation.sticky
369
+ body = generate_inline_comment_body("white_check_mark", violation, danger_id: danger_id, resolved: true, template: "gitlab")
370
+ client.update_merge_request_discussion_note(ci_source.repo_slug, ci_source.pull_request_id, comment["discussion_id"], comment["id"], body: body)
371
+ else
372
+ # We remove non-sticky violations that have no replies
373
+ # Since there's no direct concept of a reply in GH, we simply consider
374
+ # the existence of non-danger comments in that line as replies
375
+ replies = non_danger_comments.select do |potential|
376
+ potential["path"] == comment["path"] &&
377
+ potential["position"] == comment["position"] &&
378
+ potential["commit_id"] == comment["commit_id"]
379
+ end
380
+
381
+ client.delete_merge_request_comment(ci_source.repo_slug, ci_source.pull_request_id, comment["id"]) if replies.empty?
382
+ end
383
+ end
384
+
385
+ {
386
+ warnings: warnings,
387
+ errors: errors,
388
+ messages: messages,
389
+ markdowns: markdowns
390
+ }
391
+ end
392
+
393
+ def submit_inline_comments_for_kind!(kind, messages, _diff_lines, danger_comments, previous_violations, danger_id: "danger")
394
+ previous_violations ||= []
395
+ is_markdown_content = kind == :markdown
396
+ emoji = { warning: "warning", error: "no_entry_sign", message: "book" }[kind]
397
+
398
+ messages.reject do |m|
399
+ next false unless m.file && m.line
400
+ # Reject if it's out of range and in dismiss mode
401
+ next true if dismiss_out_of_range_messages_for(kind) && is_out_of_range(mr_changes.changes, m)
402
+
403
+ # Once we know we're gonna submit it, we format it
404
+ if is_markdown_content
405
+ body = generate_inline_markdown_body(m, danger_id: danger_id, template: "gitlab")
406
+ else
407
+ # Hide the inline link behind a span
408
+ m = process_markdown(m, true)
409
+ body = generate_inline_comment_body(emoji, m, danger_id: danger_id, template: "gitlab")
410
+ # A comment might be in previous_violations because only now it's part of the unified diff
411
+ # We remove from the array since it won't have a place in the table anymore
412
+ previous_violations.reject! { |v| messages_are_equivalent(v, m) }
413
+ end
414
+
415
+ matching_comments = danger_comments.select do |comment_data|
416
+ position = comment_data["position"]
417
+
418
+ if position.nil?
419
+ false
420
+ else
421
+ position["new_path"] == m.file && position["new_line"] == m.line
422
+ end
423
+ end
424
+
425
+ if matching_comments.empty?
426
+ old_position = find_old_position_in_diff mr_changes.changes, m
427
+ next false if old_position.nil?
428
+
429
+ params = {
430
+ body: body,
431
+ position: {
432
+ position_type: "text",
433
+ new_path: m.file,
434
+ new_line: m.line,
435
+ old_path: old_position[:path],
436
+ old_line: old_position[:line],
437
+ base_sha: self.mr_json.diff_refs.base_sha,
438
+ start_sha: self.mr_json.diff_refs.start_sha,
439
+ head_sha: self.mr_json.diff_refs.head_sha
440
+ }
441
+ }
442
+ begin
443
+ client.create_merge_request_discussion(ci_source.repo_slug, ci_source.pull_request_id, params)
444
+ rescue Gitlab::Error::Error => e
445
+ message = [e, "body: #{body}", "position: #{params[:position].inspect}"].join("\n")
446
+ puts message
447
+
448
+ next false
449
+ end
450
+ else
451
+ # Remove the surviving comment so we don't strike it out
452
+ danger_comments.reject! { |c| matching_comments.include? c }
453
+
454
+ # Update the comment to remove the strikethrough if present
455
+ comment = matching_comments.first
456
+ begin
457
+ client.update_merge_request_discussion_note(ci_source.repo_slug, ci_source.pull_request_id, comment["discussion_id"], comment["id"], body: body)
458
+ rescue Gitlab::Error::Error => e
459
+ message = [e, "body: #{body}"].join("\n")
460
+ puts message
461
+
462
+ next false
463
+ end
464
+ end
465
+
466
+ # Remove this element from the array
467
+ next true
468
+ end
469
+ end
470
+
471
+ def find_old_position_in_diff(changes, message)
472
+ range_header_regexp = /@@ -(?<old>[0-9]+)(,([0-9]+))? \+(?<new>[0-9]+)(,([0-9]+))? @@.*/
473
+
474
+ change = changes.find { |c| c["new_path"] == message.file }
475
+ # If there is no changes or rename only or deleted, return nil.
476
+ return nil if change.nil? || change["diff"].empty? || change["deleted_file"]
477
+
478
+ modified_position = {
479
+ path: change["old_path"],
480
+ line: nil
481
+ }
482
+
483
+ # If the file is new one, old line number must be nil.
484
+ return modified_position if change["new_file"]
485
+
486
+ current_old_line = 0
487
+ current_new_line = 0
488
+
489
+ change["diff"].each_line do |line|
490
+ match = line.match range_header_regexp
491
+
492
+ if match
493
+ # If the message line is at before next diffs, break from loop.
494
+ break if message.line.to_i < match[:new].to_i
495
+
496
+ # The match [:old] line does not appear yet at the header position, so reduce line number.
497
+ current_old_line = match[:old].to_i - 1
498
+ current_new_line = match[:new].to_i - 1
499
+ next
500
+ end
501
+
502
+ if line.start_with?("-")
503
+ current_old_line += 1
504
+ elsif line.start_with?("+")
505
+ current_new_line += 1
506
+ # If the message line starts with '+', old line number must be nil.
507
+ return modified_position if current_new_line == message.line.to_i
508
+ elsif !line.eql?("\\n")
509
+ current_old_line += 1
510
+ current_new_line += 1
511
+ # If the message line doesn't start with '+', old line number must be specified.
512
+ break if current_new_line == message.line.to_i
513
+ end
514
+ end
515
+
516
+ {
517
+ path: change["old_path"],
518
+ line: current_old_line - current_new_line + message.line.to_i
519
+ }
520
+ end
521
+
522
+ def is_out_of_range(changes, message)
523
+ change = changes.find { |c| c["new_path"] == message.file }
524
+ # If there is no changes or rename only or deleted, return out of range.
525
+ return true if change.nil? || change["diff"].empty? || change["deleted_file"]
526
+
527
+ # If new file then return in range
528
+ return false if change["new_file"]
529
+
530
+ addition_lines = generate_addition_lines(change["diff"])
531
+ return false if addition_lines.include?(message.line.to_i)
532
+
533
+ return true
534
+ end
535
+
536
+ def generate_addition_lines(diff)
537
+ range_header_regexp = /@@ -(?<old>[0-9]+)(,([0-9]+))? \+(?<new>[0-9]+)(,([0-9]+))? @@.*/
538
+ addition_lines = []
539
+ line_number = 0
540
+ diff.each_line do |line|
541
+ if line.match range_header_regexp
542
+ line = line.split("+").last
543
+ line = line.split(" ").first
544
+ range_string = line.split(",")
545
+ line_number = range_string[0].to_i - 1
546
+ elsif line.start_with?("+")
547
+ addition_lines.push(line_number)
548
+ elsif line.start_with?("-")
549
+ line_number -= 1
550
+ end
551
+ line_number += 1
552
+ end
553
+ addition_lines
554
+ end
555
+ end
556
+ end
557
+ end
@@ -0,0 +1,50 @@
1
+ require "danger/helpers/comments_helper"
2
+ require "danger/helpers/comment"
3
+
4
+ module Danger
5
+ module RequestSources
6
+ class LocalOnly < RequestSource
7
+ include Danger::Helpers::CommentsHelper
8
+ attr_accessor :mr_json, :commits_json
9
+
10
+ def self.env_vars
11
+ ["DANGER_LOCAL_ONLY"]
12
+ end
13
+
14
+ def initialize(ci_source, _environment)
15
+ self.ci_source = ci_source
16
+ end
17
+
18
+ def validates_as_ci?
19
+ true
20
+ end
21
+
22
+ def validates_as_api_source?
23
+ true
24
+ end
25
+
26
+ def scm
27
+ @scm ||= GitRepo.new
28
+ end
29
+
30
+ def setup_danger_branches
31
+ # Check that discovered values really exists
32
+ [ci_source.base_commit, ci_source.head_commit].each do |commit|
33
+ raise "Specified commit '#{commit}' not found" if scm.exec("rev-parse --quiet --verify #{commit}").empty?
34
+ end
35
+
36
+ self.scm.exec "branch #{EnvironmentManager.danger_base_branch} #{ci_source.base_commit}"
37
+ self.scm.exec "branch #{EnvironmentManager.danger_head_branch} #{ci_source.head_commit}"
38
+ end
39
+
40
+ def fetch_details; end
41
+
42
+ def update_pull_request!(_hash_needed); end
43
+
44
+ # @return [String] The organisation name, is nil if it can't be detected
45
+ def organisation
46
+ nil
47
+ end
48
+ end
49
+ end
50
+ end