Skip to content

Commit 2455b8d

Browse files
authored
Merge pull request #1082 from pbendersky/gitlab-inline-comments
Added support for GitLab inline comments
2 parents 955f647 + 4368b51 commit 2455b8d

File tree

8 files changed

+542
-73
lines changed

8 files changed

+542
-73
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
updating an old one. [@AlexDenisov](https://github.com/AlexDenisov)
2020
Original issue: https://github.com/danger/danger/issues/1084
2121
* Use `CI_API_V4_URL` on GitLab 11.7+ installations [@glensc], #1089
22+
* Add support for inline comments on GitLab (for versions >= 10.8.0) [@pbendersky](https://github.com/pbendersky)
2223

2324
## 5.14.0
2425

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<%- @tables.each do |table| -%>
2+
<%- if table[:content].any? -%>
3+
<table data-meta="generated_by_<%= @danger_id %>">
4+
<tbody>
5+
<%- table[:content].each do |violation| -%>
6+
<tr>
7+
<td>:<%= table[:emoji] %>:</td>
8+
<td width="100%" data-sticky="<%= violation.sticky %>"><%= "<del>" if table[:resolved] %><%= violation.message %><%= "</del>" if table[:resolved] %></td>
9+
</tr>
10+
<%- end -%>
11+
</tbody>
12+
</table>
13+
<%- end -%>
14+
<%- end -%>
15+
16+
<%- @markdowns.each do |current| -%>
17+
<%= current %>
18+
<%# the previous line has to be aligned far to the left, otherwise markdown can break easily %>
19+
<%- end -%>
20+
<%# We need to add the generated_by_ to identify comments from danger. But with inlines %>
21+
<%# it might be a little annoying, so we set on the table, but if we have markdown we add the footer anyway %>
22+
<%- if @markdowns.count > 0 -%>
23+
<p align="right" data-meta="generated_by_<%= @danger_id %>">
24+
Generated by :no_entry_sign: <a href="http://danger.systems/">Danger</a>
25+
</p>
26+
<%- end -%>

lib/danger/helpers/comment.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,19 @@ def self.from_github(comment)
1212
end
1313

1414
def self.from_gitlab(comment)
15-
self.new(comment.id, comment.body)
15+
if comment.respond_to?(:id) && comment.respond_to?(:body)
16+
self.new(comment.id, comment.body)
17+
else
18+
self.new(comment["id"], comment["body"])
19+
end
1620
end
1721

1822
def generated_by_danger?(danger_id)
1923
body.include?("\"generated_by_#{danger_id}\"")
2024
end
25+
26+
def inline?
27+
body.include?("")
28+
end
2129
end
2230
end

lib/danger/request_sources/gitlab.rb

Lines changed: 252 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ class GitLab < RequestSource
1010
include Danger::Helpers::CommentsHelper
1111
attr_accessor :mr_json, :commits_json
1212

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+
1316
def self.env_vars
1417
["DANGER_GITLAB_API_TOKEN"]
1518
end
@@ -67,10 +70,21 @@ def base_commit
6770
end
6871

6972
def mr_comments
73+
# @raw_comments contains what we got back from the server.
74+
# @comments contains Comment objects (that have less information)
7075
@comments ||= begin
71-
client.merge_request_comments(ci_source.repo_slug, ci_source.pull_request_id, per_page: 100)
72-
.auto_paginate
73-
.map { |comment| Comment.from_gitlab(comment) }
76+
if supports_inline_comments
77+
@raw_comments = client.merge_request_discussions(ci_source.repo_slug, ci_source.pull_request_id)
78+
.auto_paginate
79+
.flat_map { |discussion| discussion.notes.map { |note| note.merge({"discussion_id" => discussion.id}) } }
80+
@raw_comments
81+
.map { |comment| Comment.from_gitlab(comment) }
82+
else
83+
@raw_comments = client.merge_request_comments(ci_source.repo_slug, ci_source.pull_request_id, per_page: 100)
84+
.auto_paginate
85+
@raw_comments
86+
.map { |comment| Comment.from_gitlab(comment) }
87+
end
7488
end
7589
end
7690

@@ -107,7 +121,89 @@ def ignored_violations_from_pr
107121
GetIgnoredViolation.new(self.mr_json.description).call
108122
end
109123

124+
def supports_inline_comments
125+
@supports_inline_comments ||= begin
126+
# If we can't check GitLab's version, we assume we don't support inline comments
127+
if Gem.loaded_specs["gitlab"].version < FIRST_GITLAB_GEM_WITH_VERSION_CHECK
128+
false
129+
else
130+
current_version = Gem::Version.new(client.version.version)
131+
132+
current_version >= FIRST_VERSION_WITH_INLINE_COMMENTS
133+
end
134+
end
135+
end
136+
110137
def update_pull_request!(warnings: [], errors: [], messages: [], markdowns: [], danger_id: "danger", new_comment: false, remove_previous_comments: false)
138+
if supports_inline_comments
139+
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)
140+
else
141+
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)
142+
end
143+
end
144+
145+
def update_pull_request_with_inline_comments!(warnings: [], errors: [], messages: [], markdowns: [], danger_id: "danger", new_comment: false, remove_previous_comments: false)
146+
editable_comments = mr_comments.select { |comment| comment.generated_by_danger?(danger_id) }
147+
148+
last_comment = editable_comments.last
149+
should_create_new_comment = new_comment || last_comment.nil? || remove_previous_comments
150+
151+
previous_violations =
152+
if should_create_new_comment
153+
{}
154+
else
155+
parse_comment(last_comment.body)
156+
end
157+
158+
regular_violations = regular_violations_group(
159+
warnings: warnings,
160+
errors: errors,
161+
messages: messages,
162+
markdowns: markdowns
163+
)
164+
165+
inline_violations = inline_violations_group(
166+
warnings: warnings,
167+
errors: errors,
168+
messages: messages,
169+
markdowns: markdowns
170+
)
171+
172+
rest_inline_violations = submit_inline_comments!({
173+
danger_id: danger_id,
174+
previous_violations: previous_violations
175+
}.merge(inline_violations))
176+
177+
main_violations = merge_violations(
178+
regular_violations, rest_inline_violations
179+
)
180+
181+
main_violations_sum = main_violations.values.inject(:+)
182+
183+
if (previous_violations.empty? && main_violations_sum.empty?) || remove_previous_comments
184+
# Just remove the comment, if there's nothing to say or --remove-previous-comments CLI was set.
185+
delete_old_comments!(danger_id: danger_id)
186+
end
187+
188+
# If there are still violations to show
189+
if main_violations_sum.any?
190+
body = generate_comment({
191+
template: "gitlab",
192+
danger_id: danger_id,
193+
previous_violations: previous_violations
194+
}.merge(main_violations))
195+
196+
comment_result =
197+
if should_create_new_comment
198+
client.create_merge_request_note(ci_source.repo_slug, ci_source.pull_request_id, body)
199+
else
200+
client.edit_merge_request_note(ci_source.repo_slug, ci_source.pull_request_id, last_comment.id, body)
201+
end
202+
end
203+
204+
end
205+
206+
def update_pull_request_without_inline_comments!(warnings: [], errors: [], messages: [], markdowns: [], danger_id: "danger", new_comment: false, remove_previous_comments: false)
111207
editable_comments = mr_comments.select { |comment| comment.generated_by_danger?(danger_id) }
112208

113209
should_create_new_comment = new_comment || editable_comments.empty? || remove_previous_comments
@@ -148,14 +244,21 @@ def update_pull_request!(warnings: [], errors: [], messages: [], markdowns: [],
148244
end
149245

150246
def delete_old_comments!(except: nil, danger_id: "danger")
151-
mr_comments.each do |comment|
247+
@raw_comments.each do |raw_comment|
248+
249+
comment = Comment.from_gitlab(raw_comment)
152250
next unless comment.generated_by_danger?(danger_id)
153251
next if comment.id == except
154-
client.delete_merge_request_comment(
155-
ci_source.repo_slug,
156-
ci_source.pull_request_id,
157-
comment.id
158-
)
252+
next unless raw_comment.is_a?(Hash) && raw_comment["position"].nil?
253+
254+
begin
255+
client.delete_merge_request_comment(
256+
ci_source.repo_slug,
257+
ci_source.pull_request_id,
258+
comment.id
259+
)
260+
rescue
261+
end
159262
end
160263
end
161264

@@ -170,6 +273,146 @@ def file_url(organisation: nil, repository: nil, branch: nil, path: nil)
170273

171274
"https://#{host}/#{organisation}/#{repository}/raw/#{branch}/#{path}"
172275
end
276+
277+
def regular_violations_group(warnings: [], errors: [], messages: [], markdowns: [])
278+
{
279+
warnings: warnings.reject(&:inline?),
280+
errors: errors.reject(&:inline?),
281+
messages: messages.reject(&:inline?),
282+
markdowns: markdowns.reject(&:inline?)
283+
}
284+
end
285+
286+
def inline_violations_group(warnings: [], errors: [], messages: [], markdowns: [])
287+
cmp = proc do |a, b|
288+
next -1 unless a.file && a.line
289+
next 1 unless b.file && b.line
290+
291+
next a.line <=> b.line if a.file == b.file
292+
next a.file <=> b.file
293+
end
294+
295+
# Sort to group inline comments by file
296+
{
297+
warnings: warnings.select(&:inline?).sort(&cmp),
298+
errors: errors.select(&:inline?).sort(&cmp),
299+
messages: messages.select(&:inline?).sort(&cmp),
300+
markdowns: markdowns.select(&:inline?).sort(&cmp)
301+
}
302+
end
303+
304+
def merge_violations(*violation_groups)
305+
violation_groups.inject({}) do |accumulator, group|
306+
accumulator.merge(group) { |_, old, fresh| old + fresh }
307+
end
308+
end
309+
310+
def submit_inline_comments!(warnings: [], errors: [], messages: [], markdowns: [], previous_violations: [], danger_id: "danger")
311+
comments = client.merge_request_discussions(ci_source.repo_slug, ci_source.pull_request_id)
312+
.auto_paginate
313+
.flat_map { |discussion| discussion.notes.map { |note| note.merge({"discussion_id" => discussion.id}) } }
314+
315+
danger_comments = comments.select { |comment| Comment.from_gitlab(comment).generated_by_danger?(danger_id) }
316+
non_danger_comments = comments - danger_comments
317+
318+
diff_lines = []
319+
320+
warnings = submit_inline_comments_for_kind!(:warning, warnings, diff_lines, danger_comments, previous_violations["warning"], danger_id: danger_id)
321+
errors = submit_inline_comments_for_kind!(:error, errors, diff_lines, danger_comments, previous_violations["error"], danger_id: danger_id)
322+
messages = submit_inline_comments_for_kind!(:message, messages, diff_lines, danger_comments, previous_violations["message"], danger_id: danger_id)
323+
markdowns = submit_inline_comments_for_kind!(:markdown, markdowns, diff_lines, danger_comments, [], danger_id: danger_id)
324+
325+
# submit removes from the array all comments that are still in force
326+
# so we strike out all remaining ones
327+
danger_comments.each do |comment|
328+
violation = violations_from_table(comment["body"]).first
329+
if !violation.nil? && violation.sticky
330+
body = generate_inline_comment_body("white_check_mark", violation, danger_id: danger_id, resolved: true, template: "gitlab")
331+
client.update_merge_request_discussion_note(ci_source.repo_slug, ci_source.pull_request_id, comment["discussion_id"], comment["id"], body)
332+
else
333+
# We remove non-sticky violations that have no replies
334+
# Since there's no direct concept of a reply in GH, we simply consider
335+
# the existance of non-danger comments in that line as replies
336+
replies = non_danger_comments.select do |potential|
337+
potential["path"] == comment["path"] &&
338+
potential["position"] == comment["position"] &&
339+
potential["commit_id"] == comment["commit_id"]
340+
end
341+
342+
client.delete_merge_request_comment(ci_source.repo_slug, ci_source.pull_request_id, comment["id"]) if replies.empty?
343+
end
344+
end
345+
346+
{
347+
warnings: warnings,
348+
errors: errors,
349+
messages: messages,
350+
markdowns: markdowns
351+
}
352+
end
353+
354+
def submit_inline_comments_for_kind!(kind, messages, diff_lines, danger_comments, previous_violations, danger_id: "danger")
355+
previous_violations ||= []
356+
is_markdown_content = kind == :markdown
357+
emoji = { warning: "warning", error: "no_entry_sign", message: "book" }[kind]
358+
359+
messages.reject do |m|
360+
next false unless m.file && m.line
361+
362+
# position = find_position_in_diff diff_lines, m, kind
363+
364+
# Keep the change if it's line is not in the diff and not in dismiss mode
365+
# next dismiss_out_of_range_messages_for(kind) if position.nil?
366+
367+
# Once we know we're gonna submit it, we format it
368+
if is_markdown_content
369+
body = generate_inline_markdown_body(m, danger_id: danger_id, template: "gitlab")
370+
else
371+
# Hide the inline link behind a span
372+
m = process_markdown(m, true)
373+
body = generate_inline_comment_body(emoji, m, danger_id: danger_id, template: "gitlab")
374+
# A comment might be in previous_violations because only now it's part of the unified diff
375+
# We remove from the array since it won't have a place in the table anymore
376+
previous_violations.reject! { |v| messages_are_equivalent(v, m) }
377+
end
378+
379+
matching_comments = danger_comments.select do |comment_data|
380+
position = comment_data["position"]
381+
382+
if position.nil?
383+
false
384+
else
385+
position["new_path"] == m.file && position["new_line"] == m.line
386+
end
387+
end
388+
389+
if matching_comments.empty?
390+
params = {
391+
body: body,
392+
position: {
393+
position_type: 'text',
394+
new_path: m.file,
395+
new_line: m.line,
396+
base_sha: self.mr_json.diff_refs.base_sha,
397+
start_sha: self.mr_json.diff_refs.start_sha,
398+
head_sha: self.mr_json.diff_refs.head_sha
399+
}
400+
}
401+
client.create_merge_request_discussion(ci_source.repo_slug, ci_source.pull_request_id, params)
402+
else
403+
# Remove the surviving comment so we don't strike it out
404+
danger_comments.reject! { |c| matching_comments.include? c }
405+
406+
# Update the comment to remove the strikethrough if present
407+
comment = matching_comments.first
408+
client.update_merge_request_discussion_note(ci_source.repo_slug, ci_source.pull_request_id, comment["discussion_id"], comment["id"], body)
409+
end
410+
411+
# Remove this element from the array
412+
next true
413+
end
414+
end
415+
173416
end
174417
end
175418
end
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
HTTP/1.1 200 OK
2+
Server: nginx
3+
Date: Thu, 14 Feb 2019 20:13:19 GMT
4+
Content-Type: application/json
5+
Content-Length: 7789
6+
Cache-Control: max-age=0, private, must-revalidate
7+
Etag: W/"5b77db1f9b56d74e705229cc87b3d5a6"
8+
Link: <https://gitlab.com/api/v4/projects/k0nserv%2Fdanger-test/merge_requests/1/discussions?id=k0nserv%2Fdanger-test&noteable_id=1&page=1&per_page=20>; rel="first", <https://gitlab.com/api/v4/projects/k0nserv%2Fdanger-test/merge_requests/1/discussions?id=k0nserv%2Fdanger-test&noteable_id=1&page=1&per_page=20>; rel="last"
9+
Vary: Origin
10+
X-Content-Type-Options: nosniff
11+
X-Frame-Options: SAMEORIGIN
12+
X-Next-Page:
13+
X-Page: 1
14+
X-Per-Page: 20
15+
X-Prev-Page:
16+
X-Request-Id: OCKNUgQKAc3
17+
X-Runtime: 0.189194
18+
X-Total: 9
19+
X-Total-Pages: 1
20+
Strict-Transport-Security: max-age=31536000
21+
RateLimit-Limit: 600
22+
RateLimit-Observed: 1
23+
RateLimit-Remaining: 599
24+
RateLimit-Reset: 1550175259
25+
RateLimit-ResetTime: Fri, 14 Feb 2019 20:14:19 GMT
26+
27+
[]

0 commit comments

Comments
 (0)