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,126 @@
1
+ # coding: utf-8
2
+
3
+ require "octokit"
4
+ require "danger/ci_source/ci_source"
5
+ require "danger/request_sources/github/github_review_resolver"
6
+ require "danger/danger_core/messages/violation"
7
+ require "danger/danger_core/messages/markdown"
8
+ require "danger/helpers/comments_helper"
9
+ require "danger/helpers/comment"
10
+
11
+ module Danger
12
+ module RequestSources
13
+ module GitHubSource
14
+ class Review
15
+ include Danger::Helpers::CommentsHelper
16
+
17
+ # @see https://developer.github.com/v3/pulls/reviews/ for all possible events
18
+ EVENT_APPROVE = "APPROVE".freeze
19
+ EVENT_REQUEST_CHANGES = "REQUEST_CHANGES".freeze
20
+ EVENT_COMMENT = "COMMENT".freeze
21
+
22
+ # Current review status, if the review has not been submitted yet -> STATUS_PENDING
23
+ STATUS_APPROVED = "APPROVED".freeze
24
+ STATUS_REQUESTED_CHANGES = "CHANGES_REQUESTED".freeze
25
+ STATUS_COMMENTED = "COMMENTED".freeze
26
+ STATUS_PENDING = "PENDING".freeze
27
+
28
+ attr_reader :id, :body, :status, :review_json
29
+
30
+ def initialize(client, ci_source, review_json = nil)
31
+ @ci_source = ci_source
32
+ @client = client
33
+ @review_json = review_json
34
+ end
35
+
36
+ def id
37
+ return nil unless self.review_json
38
+ self.review_json["id"]
39
+ end
40
+
41
+ def body
42
+ return "" unless self.review_json
43
+ self.review_json["body"]
44
+ end
45
+
46
+ def status
47
+ return STATUS_PENDING if self.review_json.nil?
48
+ return self.review_json["state"]
49
+ end
50
+
51
+ # Starts the new review process
52
+ def start
53
+ @warnings = []
54
+ @errors = []
55
+ @messages = []
56
+ @markdowns = []
57
+ end
58
+
59
+ # Submits the prepared review
60
+ def submit
61
+ general_violations = generate_general_violations
62
+ submission_body = generate_body
63
+
64
+ # If the review resolver says that there is nothing to submit we skip submission
65
+ return unless ReviewResolver.should_submit?(self, submission_body)
66
+
67
+ @review_json = @client.create_pull_request_review(@ci_source.repo_slug, @ci_source.pull_request_id, event: generate_event(general_violations), body: submission_body)
68
+ end
69
+
70
+ def generated_by_danger?(danger_id = "danger")
71
+ self.review_json["body"].include?("generated_by_#{danger_id}")
72
+ end
73
+
74
+ def message(message, sticky = true, file = nil, line = nil)
75
+ @messages << Violation.new(message, sticky, file, line)
76
+ end
77
+
78
+ def warn(message, sticky = true, file = nil, line = nil)
79
+ @warnings << Violation.new(message, sticky, file, line)
80
+ end
81
+
82
+ def fail(message, sticky = true, file = nil, line = nil)
83
+ @errors << Violation.new(message, sticky, file, line)
84
+ end
85
+
86
+ def markdown(message, file = nil, line = nil)
87
+ @markdowns << Markdown.new(message, file, line)
88
+ end
89
+
90
+ private
91
+
92
+ # The only reason to request changes for the PR is to have errors from Danger
93
+ # otherwise let's just notify user and we're done
94
+ def generate_event(violations)
95
+ violations[:errors].empty? ? EVENT_APPROVE : EVENT_REQUEST_CHANGES
96
+ end
97
+
98
+ def generate_body(danger_id: "danger")
99
+ previous_violations = parse_comment(body)
100
+ general_violations = generate_general_violations
101
+ new_body = generate_comment(warnings: general_violations[:warnings],
102
+ errors: general_violations[:errors],
103
+ messages: general_violations[:messages],
104
+ markdowns: general_violations[:markdowns],
105
+ previous_violations: previous_violations,
106
+ danger_id: danger_id,
107
+ template: "github")
108
+ return new_body
109
+ end
110
+
111
+ def generate_general_violations
112
+ general_warnings = @warnings.reject(&:inline?)
113
+ general_errors = @errors.reject(&:inline?)
114
+ general_messages = @messages.reject(&:inline?)
115
+ general_markdowns = @markdowns.reject(&:inline?)
116
+ {
117
+ warnings: general_warnings,
118
+ markdowns: general_markdowns,
119
+ errors: general_errors,
120
+ messages: general_messages
121
+ }
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,19 @@
1
+ # coding: utf-8
2
+
3
+ require "danger/request_sources/github/github_review"
4
+
5
+ module Danger
6
+ module RequestSources
7
+ module GitHubSource
8
+ class ReviewResolver
9
+ def self.should_submit?(review, body)
10
+ return !same_body?(body, review.body)
11
+ end
12
+
13
+ def self.same_body?(body1, body2)
14
+ return !body1.nil? && !body2.nil? && body1 == body2
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+
3
+ module Danger
4
+ module RequestSources
5
+ module GitHubSource
6
+ class ReviewUnsupported
7
+ attr_reader :id, :body, :status, :review_json
8
+
9
+ def initialize; end
10
+
11
+ def start; end
12
+
13
+ def submit; end
14
+
15
+ def message(message, sticky = true, file = nil, line = nil); end
16
+
17
+ def warn(message, sticky = true, file = nil, line = nil); end
18
+
19
+ def fail(message, sticky = true, file = nil, line = nil); end
20
+
21
+ def markdown(message, file = nil, line = nil); end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,525 @@
1
+ # coding: utf-8
2
+ require "uri"
3
+ require "danger/helpers/comments_helper"
4
+ require "danger/helpers/comment"
5
+ require "danger/request_sources/support/get_ignored_violation"
6
+
7
+ module Danger
8
+ module RequestSources
9
+ class GitLab < RequestSource
10
+ include Danger::Helpers::CommentsHelper
11
+ attr_accessor :mr_json, :commits_json
12
+
13
+ FIRST_GITLAB_GEM_WITH_VERSION_CHECK = Gem::Version.new("4.6.0")
14
+ FIRST_VERSION_WITH_INLINE_COMMENTS = Gem::Version.new("10.8.0")
15
+
16
+ def self.env_vars
17
+ ["DANGER_GITLAB_API_TOKEN"]
18
+ end
19
+
20
+ def self.optional_env_vars
21
+ ["DANGER_GITLAB_HOST", "DANGER_GITLAB_API_BASE_URL"]
22
+ end
23
+
24
+ def initialize(ci_source, environment)
25
+ self.ci_source = ci_source
26
+ self.environment = environment
27
+
28
+ @token = @environment["DANGER_GITLAB_API_TOKEN"]
29
+ end
30
+
31
+ def client
32
+ token = @environment["DANGER_GITLAB_API_TOKEN"]
33
+ raise "No API token given, please provide one using `DANGER_GITLAB_API_TOKEN`" unless token
34
+
35
+ # The require happens inline so that it won't cause exceptions when just using the `danger` gem.
36
+ require "gitlab"
37
+
38
+ @client ||= Gitlab.client(endpoint: endpoint, private_token: token)
39
+ rescue LoadError => e
40
+ if e.path == "gitlab"
41
+ puts "The GitLab gem was not installed, you will need to change your Gem from `danger` to `danger-gitlab`.".red
42
+ puts "\n - See https://github.com/danger/danger/blob/master/CHANGELOG.md#400"
43
+ else
44
+ puts "Error: #{e}".red
45
+ end
46
+ abort
47
+ end
48
+
49
+ def validates_as_ci?
50
+ includes_port = self.host.include? ":"
51
+ raise "Port number included in `DANGER_GITLAB_HOST`, this will fail with GitLab CI Runners" if includes_port
52
+
53
+ # We don't call super because in some cases the Git remote doesn't match the GitLab instance host.
54
+ # In Danger::EnvironmentManager#initialize we still check that the request source is #validates_as_api_source?
55
+ # so that should be sufficient to validate GitLab as request source.
56
+ # See https://github.com/danger/danger/issues/1231 and https://gitlab.com/gitlab-com/gl-infra/infrastructure/-/issues/10069.
57
+ true
58
+ end
59
+
60
+ def validates_as_api_source?
61
+ @token && !@token.empty?
62
+ end
63
+
64
+ def scm
65
+ @scm ||= GitRepo.new
66
+ end
67
+
68
+ def endpoint
69
+ @endpoint ||= @environment["DANGER_GITLAB_API_BASE_URL"] || @environment["CI_API_V4_URL"] || "https://gitlab.com/api/v4"
70
+ end
71
+
72
+ def host
73
+ @host ||= @environment["DANGER_GITLAB_HOST"] || URI.parse(endpoint).host || "gitlab.com"
74
+ end
75
+
76
+ def base_commit
77
+ @base_commit ||= self.mr_json.diff_refs.base_sha
78
+ end
79
+
80
+ def mr_comments
81
+ # @raw_comments contains what we got back from the server.
82
+ # @comments contains Comment objects (that have less information)
83
+ @comments ||= begin
84
+ if supports_inline_comments
85
+ @raw_comments = mr_discussions
86
+ .auto_paginate
87
+ .flat_map { |discussion| discussion.notes.map { |note| note.merge({"discussion_id" => discussion.id}) } }
88
+ @raw_comments
89
+ .map { |comment| Comment.from_gitlab(comment) }
90
+ else
91
+ @raw_comments = client.merge_request_comments(ci_source.repo_slug, ci_source.pull_request_id, per_page: 100)
92
+ .auto_paginate
93
+ @raw_comments
94
+ .map { |comment| Comment.from_gitlab(comment) }
95
+ end
96
+ end
97
+ end
98
+
99
+ def mr_discussions
100
+ @mr_discussions ||= client.merge_request_discussions(ci_source.repo_slug, ci_source.pull_request_id)
101
+ end
102
+
103
+ def mr_diff
104
+ @mr_diff ||= begin
105
+ diffs = mr_changes.changes.map do |change|
106
+ diff = change["diff"]
107
+ if diff.start_with?('--- a/')
108
+ diff
109
+ else
110
+ "--- a/#{change["old_path"]}\n+++ b/#{change["new_path"]}\n#{diff}"
111
+ end
112
+ end
113
+ diffs.join("\n")
114
+ end
115
+ end
116
+
117
+ def mr_changed_paths
118
+ @mr_changed_paths ||= begin
119
+ mr_changes
120
+ .changes.map { |change| change["new_path"] }
121
+ end
122
+
123
+ @mr_changed_paths
124
+ end
125
+
126
+ def mr_changes
127
+ @mr_changes ||= begin
128
+ client.merge_request_changes(ci_source.repo_slug, ci_source.pull_request_id)
129
+ end
130
+ end
131
+
132
+ def setup_danger_branches
133
+ # we can use a GitLab specific feature here:
134
+ base_branch = self.mr_json.source_branch
135
+ base_commit = self.mr_json.diff_refs.base_sha
136
+ head_branch = self.mr_json.target_branch
137
+ head_commit = self.mr_json.diff_refs.head_sha
138
+
139
+ # Next, we want to ensure that we have a version of the current branch at a known location
140
+ scm.ensure_commitish_exists_on_branch! base_branch, base_commit
141
+ self.scm.exec "branch #{EnvironmentManager.danger_base_branch} #{base_commit}"
142
+
143
+ # OK, so we want to ensure that we have a known head branch, this will always represent
144
+ # the head of the PR ( e.g. the most recent commit that will be merged. )
145
+ scm.ensure_commitish_exists_on_branch! head_branch, head_commit
146
+ self.scm.exec "branch #{EnvironmentManager.danger_head_branch} #{head_commit}"
147
+ end
148
+
149
+ def fetch_details
150
+ self.mr_json = client.merge_request(ci_source.repo_slug, self.ci_source.pull_request_id)
151
+ self.ignored_violations = ignored_violations_from_pr
152
+ end
153
+
154
+ def ignored_violations_from_pr
155
+ GetIgnoredViolation.new(self.mr_json.description).call
156
+ end
157
+
158
+ def supports_inline_comments
159
+ @supports_inline_comments ||= begin
160
+ # If we can't check GitLab's version, we assume we don't support inline comments
161
+ if Gem.loaded_specs["gitlab"].version < FIRST_GITLAB_GEM_WITH_VERSION_CHECK
162
+ false
163
+ else
164
+ current_version = Gem::Version.new(client.version.version)
165
+
166
+ current_version >= FIRST_VERSION_WITH_INLINE_COMMENTS
167
+ end
168
+ end
169
+ end
170
+
171
+ def update_pull_request!(warnings: [], errors: [], messages: [], markdowns: [], danger_id: "danger", new_comment: false, remove_previous_comments: false)
172
+ if supports_inline_comments
173
+ 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)
174
+ else
175
+ 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)
176
+ end
177
+ end
178
+
179
+ def update_pull_request_with_inline_comments!(warnings: [], errors: [], messages: [], markdowns: [], danger_id: "danger", new_comment: false, remove_previous_comments: false)
180
+ editable_regular_comments = mr_comments
181
+ .select { |comment| comment.generated_by_danger?(danger_id) }
182
+ .reject(&:inline?)
183
+
184
+ last_comment = editable_regular_comments.last
185
+ should_create_new_comment = new_comment || last_comment.nil? || remove_previous_comments
186
+
187
+ previous_violations =
188
+ if should_create_new_comment
189
+ {}
190
+ else
191
+ parse_comment(last_comment.body)
192
+ end
193
+
194
+ regular_violations = regular_violations_group(
195
+ warnings: warnings,
196
+ errors: errors,
197
+ messages: messages,
198
+ markdowns: markdowns
199
+ )
200
+
201
+ inline_violations = inline_violations_group(
202
+ warnings: warnings,
203
+ errors: errors,
204
+ messages: messages,
205
+ markdowns: markdowns
206
+ )
207
+
208
+ rest_inline_violations = submit_inline_comments!({
209
+ danger_id: danger_id,
210
+ previous_violations: previous_violations
211
+ }.merge(inline_violations))
212
+
213
+ main_violations = merge_violations(
214
+ regular_violations, rest_inline_violations
215
+ )
216
+
217
+ main_violations_sum = main_violations.values.inject(:+)
218
+
219
+ if (previous_violations.empty? && main_violations_sum.empty?) || remove_previous_comments
220
+ # Just remove the comment, if there's nothing to say or --remove-previous-comments CLI was set.
221
+ delete_old_comments!(danger_id: danger_id)
222
+ end
223
+
224
+ # If there are still violations to show
225
+ if main_violations_sum.any?
226
+ body = generate_comment({
227
+ template: "gitlab",
228
+ danger_id: danger_id,
229
+ previous_violations: previous_violations
230
+ }.merge(main_violations))
231
+
232
+ comment_result =
233
+ if should_create_new_comment
234
+ client.create_merge_request_note(ci_source.repo_slug, ci_source.pull_request_id, body)
235
+ else
236
+ client.edit_merge_request_note(ci_source.repo_slug, ci_source.pull_request_id, last_comment.id, body)
237
+ end
238
+ end
239
+ end
240
+
241
+ def update_pull_request_without_inline_comments!(warnings: [], errors: [], messages: [], markdowns: [], danger_id: "danger", new_comment: false, remove_previous_comments: false)
242
+ editable_comments = mr_comments.select { |comment| comment.generated_by_danger?(danger_id) }
243
+
244
+ should_create_new_comment = new_comment || editable_comments.empty? || remove_previous_comments
245
+
246
+ if should_create_new_comment
247
+ previous_violations = {}
248
+ else
249
+ comment = editable_comments.first.body
250
+ previous_violations = parse_comment(comment)
251
+ end
252
+
253
+ if (previous_violations.empty? && (warnings + errors + messages + markdowns).empty?) || remove_previous_comments
254
+ # Just remove the comment, if there's nothing to say or --remove-previous-comments CLI was set.
255
+ delete_old_comments!(danger_id: danger_id)
256
+ else
257
+ body = generate_comment(warnings: warnings,
258
+ errors: errors,
259
+ messages: messages,
260
+ markdowns: markdowns,
261
+ previous_violations: previous_violations,
262
+ danger_id: danger_id,
263
+ template: "gitlab")
264
+
265
+ if editable_comments.empty? or should_create_new_comment
266
+ client.create_merge_request_comment(
267
+ ci_source.repo_slug, ci_source.pull_request_id, body
268
+ )
269
+ else
270
+ original_id = editable_comments.first.id
271
+ client.edit_merge_request_comment(
272
+ ci_source.repo_slug,
273
+ ci_source.pull_request_id,
274
+ original_id,
275
+ { body: body }
276
+ )
277
+ end
278
+ end
279
+ end
280
+
281
+ def delete_old_comments!(except: nil, danger_id: "danger")
282
+ @raw_comments.each do |raw_comment|
283
+
284
+ comment = Comment.from_gitlab(raw_comment)
285
+ next unless comment.generated_by_danger?(danger_id)
286
+ next if comment.id == except
287
+ next unless raw_comment.is_a?(Hash) && raw_comment["position"].nil?
288
+
289
+ begin
290
+ client.delete_merge_request_comment(
291
+ ci_source.repo_slug,
292
+ ci_source.pull_request_id,
293
+ comment.id
294
+ )
295
+ rescue
296
+ end
297
+ end
298
+ end
299
+
300
+ # @return [String] The organisation name, is nil if it can't be detected
301
+ def organisation
302
+ nil # TODO: Implement this
303
+ end
304
+
305
+ # @return [String] A URL to the specific file, ready to be downloaded
306
+ def file_url(organisation: nil, repository: nil, branch: nil, path: nil)
307
+ branch ||= 'master'
308
+ token = @environment["DANGER_GITLAB_API_TOKEN"]
309
+ # According to GitLab Repositories API docs path and id(slug) should be encoded.
310
+ path = URI.encode_www_form_component(path)
311
+ repository = URI.encode_www_form_component(repository)
312
+ "#{endpoint}/projects/#{repository}/repository/files/#{path}/raw?ref=#{branch}&private_token=#{token}"
313
+ end
314
+
315
+ def regular_violations_group(warnings: [], errors: [], messages: [], markdowns: [])
316
+ {
317
+ warnings: warnings.reject(&:inline?),
318
+ errors: errors.reject(&:inline?),
319
+ messages: messages.reject(&:inline?),
320
+ markdowns: markdowns.reject(&:inline?)
321
+ }
322
+ end
323
+
324
+ def inline_violations_group(warnings: [], errors: [], messages: [], markdowns: [])
325
+ cmp = proc do |a, b|
326
+ next -1 unless a.file && a.line
327
+ next 1 unless b.file && b.line
328
+
329
+ next a.line <=> b.line if a.file == b.file
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.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
+
401
+ # Keep the change it's in a file changed in this diff
402
+ next if !mr_changed_paths.include?(m.file)
403
+
404
+ # Once we know we're gonna submit it, we format it
405
+ if is_markdown_content
406
+ body = generate_inline_markdown_body(m, danger_id: danger_id, template: "gitlab")
407
+ else
408
+ # Hide the inline link behind a span
409
+ m = process_markdown(m, true)
410
+ body = generate_inline_comment_body(emoji, m, danger_id: danger_id, template: "gitlab")
411
+ # A comment might be in previous_violations because only now it's part of the unified diff
412
+ # We remove from the array since it won't have a place in the table anymore
413
+ previous_violations.reject! { |v| messages_are_equivalent(v, m) }
414
+ end
415
+
416
+ matching_comments = danger_comments.select do |comment_data|
417
+ position = comment_data["position"]
418
+
419
+ if position.nil?
420
+ false
421
+ else
422
+ position["new_path"] == m.file && position["new_line"] == m.line
423
+ end
424
+ end
425
+
426
+ if matching_comments.empty?
427
+ old_position = find_old_position_in_diff mr_changes.changes, m
428
+ next false if old_position.nil?
429
+
430
+ params = {
431
+ body: body,
432
+ position: {
433
+ position_type: 'text',
434
+ new_path: m.file,
435
+ new_line: m.line,
436
+ old_path: old_position[:path],
437
+ old_line: old_position[:line],
438
+ base_sha: self.mr_json.diff_refs.base_sha,
439
+ start_sha: self.mr_json.diff_refs.start_sha,
440
+ head_sha: self.mr_json.diff_refs.head_sha
441
+ }
442
+ }
443
+ begin
444
+ client.create_merge_request_discussion(ci_source.repo_slug, ci_source.pull_request_id, params)
445
+ rescue Gitlab::Error::Error => e
446
+ message = [e, "body: #{body}", "position: #{params[:position].inspect}"].join("\n")
447
+ puts message
448
+
449
+ next false
450
+ end
451
+ else
452
+ # Remove the surviving comment so we don't strike it out
453
+ danger_comments.reject! { |c| matching_comments.include? c }
454
+
455
+ # Update the comment to remove the strikethrough if present
456
+ comment = matching_comments.first
457
+ begin
458
+ client.update_merge_request_discussion_note(ci_source.repo_slug, ci_source.pull_request_id, comment["discussion_id"], comment["id"], body: body)
459
+ rescue Gitlab::Error::Error => e
460
+ message = [e, "body: #{body}"].join("\n")
461
+ puts message
462
+
463
+ next false
464
+ end
465
+ end
466
+
467
+ # Remove this element from the array
468
+ next true
469
+ end
470
+ end
471
+
472
+ def find_old_position_in_diff(changes, message)
473
+ range_header_regexp = /@@ -(?<old>[0-9]+)(,([0-9]+))? \+(?<new>[0-9]+)(,([0-9]+))? @@.*/
474
+
475
+ change = changes.find { |c| c["new_path"] == message.file }
476
+
477
+ # If there is no changes or rename only or deleted, return nil.
478
+ return nil if change.nil? || change["diff"].empty? || change["deleted_file"]
479
+
480
+ modified_position = {
481
+ path: change["old_path"],
482
+ line: nil
483
+ }
484
+
485
+ # If the file is new one, old line number must be nil.
486
+ return modified_position if change["new_file"]
487
+
488
+ current_old_line = 0
489
+ current_new_line = 0
490
+
491
+ change["diff"].each_line do |line|
492
+ match = line.match range_header_regexp
493
+
494
+ if match
495
+ # If the message line is at before next diffs, break from loop.
496
+ break if message.line.to_i < match[:new].to_i
497
+
498
+ # The match [:old] line does not appear yet at the header position, so reduce line number.
499
+ current_old_line = match[:old].to_i - 1
500
+ current_new_line = match[:new].to_i - 1
501
+ next
502
+ end
503
+
504
+ if line.start_with?("-")
505
+ current_old_line += 1
506
+ elsif line.start_with?("+")
507
+ current_new_line += 1
508
+ # If the message line starts with '+', old line number must be nil.
509
+ return modified_position if current_new_line == message.line.to_i
510
+ elsif !line.eql?("\\n")
511
+ current_old_line += 1
512
+ current_new_line += 1
513
+ # If the message line doesn't start with '+', old line number must be specified.
514
+ break if current_new_line == message.line.to_i
515
+ end
516
+ end
517
+
518
+ {
519
+ path: change["old_path"],
520
+ line: current_old_line - current_new_line + message.line.to_i
521
+ }
522
+ end
523
+ end
524
+ end
525
+ end