danger 8.0.3

Sign up to get free protection for your applications and to get access to all the features.
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 +159 -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