danger 3.1.1 → 3.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/danger.rb +14 -0
- data/lib/danger/ci_source/jenkins.rb +1 -1
- data/lib/danger/ci_source/local_git_repo.rb +4 -0
- data/lib/danger/clients/rubygems_client.rb +14 -0
- data/lib/danger/comment_generators/bitbucket_server.md.erb +20 -0
- data/lib/danger/comment_generators/github_inline.md.erb +26 -0
- data/lib/danger/danger_core/dangerfile.rb +15 -6
- data/lib/danger/danger_core/messages/markdown.rb +28 -0
- data/lib/danger/danger_core/messages/violation.rb +35 -0
- data/lib/danger/danger_core/plugins/dangerfile_bitbucket_server_plugin.rb +182 -0
- data/lib/danger/danger_core/plugins/dangerfile_danger_plugin.rb +61 -6
- data/lib/danger/danger_core/plugins/dangerfile_messaging_plugin.rb +30 -9
- data/lib/danger/danger_core/standard_error.rb +10 -1
- data/lib/danger/helpers/comments_helper.rb +94 -33
- data/lib/danger/helpers/comments_parsing_helper.rb +52 -0
- data/lib/danger/request_source/bitbucket_server.rb +78 -0
- data/lib/danger/request_source/bitbucket_server_api.rb +70 -0
- data/lib/danger/request_source/github.rb +207 -9
- data/lib/danger/scm_source/git_repo.rb +10 -1
- data/lib/danger/version.rb +1 -1
- metadata +13 -4
- data/lib/danger/danger_core/violation.rb +0 -10
@@ -1,4 +1,5 @@
|
|
1
|
-
require "danger/danger_core/violation"
|
1
|
+
require "danger/danger_core/messages/violation"
|
2
|
+
require "danger/danger_core/messages/markdown"
|
2
3
|
require "danger/plugin_support/plugin"
|
3
4
|
|
4
5
|
module Danger
|
@@ -38,6 +39,10 @@ module Danger
|
|
38
39
|
# message << "20 | No documentation | Error \n"
|
39
40
|
# markdown message
|
40
41
|
#
|
42
|
+
# @example Adding an inline warning to a file
|
43
|
+
#
|
44
|
+
# warn("You shouldn't use puts in your Dangerfile", file: "Dangerfile", line: 10)
|
45
|
+
#
|
41
46
|
#
|
42
47
|
# @see danger/danger
|
43
48
|
# @tags core, messaging
|
@@ -65,10 +70,14 @@ module Danger
|
|
65
70
|
#
|
66
71
|
# @param [String] message
|
67
72
|
# The markdown based message to be printed below the table
|
73
|
+
# @param [String] file
|
74
|
+
# Optional. Path to the file that the message is for.
|
75
|
+
# @param [String] line
|
76
|
+
# Optional. The line in the file to present the message in.
|
68
77
|
# @return [void]
|
69
78
|
#
|
70
|
-
def markdown(message)
|
71
|
-
@markdowns << message
|
79
|
+
def markdown(message, file: nil, line: nil)
|
80
|
+
@markdowns << Markdown.new(message, file, line)
|
72
81
|
end
|
73
82
|
|
74
83
|
# @!group Core
|
@@ -79,10 +88,14 @@ module Danger
|
|
79
88
|
# @param [Boolean] sticky
|
80
89
|
# Whether the message should be kept after it was fixed,
|
81
90
|
# defaults to `true`.
|
91
|
+
# @param [String] file
|
92
|
+
# Optional. Path to the file that the message is for.
|
93
|
+
# @param [String] line
|
94
|
+
# Optional. The line in the file to present the message in.
|
82
95
|
# @return [void]
|
83
96
|
#
|
84
|
-
def message(message, sticky: true)
|
85
|
-
@messages << Violation.new(message, sticky)
|
97
|
+
def message(message, sticky: true, file: nil, line: nil)
|
98
|
+
@messages << Violation.new(message, sticky, file, line)
|
86
99
|
end
|
87
100
|
|
88
101
|
# @!group Core
|
@@ -93,11 +106,15 @@ module Danger
|
|
93
106
|
# @param [Boolean] sticky
|
94
107
|
# Whether the message should be kept after it was fixed,
|
95
108
|
# defaults to `true`.
|
109
|
+
# @param [String] file
|
110
|
+
# Optional. Path to the file that the message is for.
|
111
|
+
# @param [String] line
|
112
|
+
# Optional. The line in the file to present the message in.
|
96
113
|
# @return [void]
|
97
114
|
#
|
98
|
-
def warn(message, sticky: true)
|
115
|
+
def warn(message, sticky: true, file: nil, line: nil)
|
99
116
|
return if should_ignore_violation(message)
|
100
|
-
@warnings << Violation.new(message, sticky)
|
117
|
+
@warnings << Violation.new(message, sticky, file, line)
|
101
118
|
end
|
102
119
|
|
103
120
|
# @!group Core
|
@@ -108,11 +125,15 @@ module Danger
|
|
108
125
|
# @param [Boolean] sticky
|
109
126
|
# Whether the message should be kept after it was fixed,
|
110
127
|
# defaults to `true`.
|
128
|
+
# @param [String] file
|
129
|
+
# Optional. Path to the file that the message is for.
|
130
|
+
# @param [String] line
|
131
|
+
# Optional. The line in the file to present the message in.
|
111
132
|
# @return [void]
|
112
133
|
#
|
113
|
-
def fail(message, sticky: true)
|
134
|
+
def fail(message, sticky: true, file: nil, line: nil)
|
114
135
|
return if should_ignore_violation(message)
|
115
|
-
@errors << Violation.new(message, sticky)
|
136
|
+
@errors << Violation.new(message, sticky, file, line)
|
116
137
|
end
|
117
138
|
|
118
139
|
# @!group Reporting
|
@@ -63,10 +63,13 @@ module Danger
|
|
63
63
|
def message
|
64
64
|
@message ||= begin
|
65
65
|
trace_line, description = parse_line_number_from_description
|
66
|
+
latest_version = Danger.danger_outdated?
|
66
67
|
|
67
68
|
m = "\n[!] "
|
68
69
|
m << description
|
69
|
-
|
70
|
+
if latest_version
|
71
|
+
m << upgrade_message(latest_version)
|
72
|
+
end
|
70
73
|
m = m.red if m.respond_to?(:red)
|
71
74
|
|
72
75
|
return m unless backtrace && dsl_path && contents
|
@@ -103,5 +106,11 @@ module Danger
|
|
103
106
|
end
|
104
107
|
[trace_line, description]
|
105
108
|
end
|
109
|
+
|
110
|
+
def upgrade_message(latest_version)
|
111
|
+
". Updating the Danger gem might fix the issue. "\
|
112
|
+
"Your Danger version: #{Danger::VERSION}, "\
|
113
|
+
"latest Danger version: #{latest_version}\n"
|
114
|
+
end
|
106
115
|
end
|
107
116
|
end
|
@@ -1,27 +1,57 @@
|
|
1
1
|
require "kramdown"
|
2
|
+
require "danger/helpers/comments_parsing_helper"
|
3
|
+
|
4
|
+
# rubocop:disable Metrics/ModuleLength
|
2
5
|
|
3
6
|
module Danger
|
4
7
|
module Helpers
|
5
8
|
module CommentsHelper
|
9
|
+
# This might be a bit weird, but table_kind_from_title is a shared dependency for
|
10
|
+
# parsing and generating. And rubocop was adamant about file size so...
|
11
|
+
include Danger::Helpers::CommentsParsingHelper
|
12
|
+
|
6
13
|
def markdown_parser(text)
|
7
14
|
Kramdown::Document.new(text, input: "GFM")
|
8
15
|
end
|
9
16
|
|
10
|
-
|
11
|
-
|
17
|
+
# !@group Extension points
|
18
|
+
# Produces a markdown link to the file the message points to
|
19
|
+
#
|
20
|
+
# request_source implementations are invited to override this method with their
|
21
|
+
# vendor specific link.
|
22
|
+
#
|
23
|
+
# @param [Violation or Markdown] message
|
24
|
+
# @param [Bool] Should hide any generated link created
|
25
|
+
#
|
26
|
+
# @return [String] The Markdown compatible link
|
27
|
+
def markdown_link_to_message(message, _)
|
28
|
+
"#{messages.file}#L#{message.line}"
|
12
29
|
end
|
13
30
|
|
14
|
-
|
15
|
-
|
16
|
-
|
31
|
+
# !@group Extension points
|
32
|
+
# Determine whether two messages are equivalent
|
33
|
+
#
|
34
|
+
# request_source implementations are invited to override this method.
|
35
|
+
# This is mostly here to enable sources to detect when inlines change only in their
|
36
|
+
# commit hash and not in content per-se. since the link is implementation dependant
|
37
|
+
# so should be the comparision.
|
38
|
+
#
|
39
|
+
# @param [Violation or Markdown] m1
|
40
|
+
# @param [Violation or Markdown] m2
|
41
|
+
#
|
42
|
+
# @return [Boolean] whether they represent the same message
|
43
|
+
def messages_are_equivalent(m1, m2)
|
44
|
+
m1 == m2
|
17
45
|
end
|
18
46
|
|
19
|
-
def process_markdown(violation)
|
20
|
-
|
47
|
+
def process_markdown(violation, hide_link = false)
|
48
|
+
message = violation.message
|
49
|
+
message = "#{markdown_link_to_message(violation, hide_link)}#{message}" if violation.file && violation.line
|
50
|
+
|
51
|
+
html = markdown_parser(message).to_html
|
21
52
|
# Remove the outer `<p>`, the -5 represents a newline + `</p>`
|
22
53
|
html = html[3...-5] if html.start_with? "<p>"
|
23
|
-
|
24
|
-
Violation.new(html, violation.sticky)
|
54
|
+
Violation.new(html, violation.sticky, violation.file, violation.line)
|
25
55
|
end
|
26
56
|
|
27
57
|
def parse_comment(comment)
|
@@ -41,50 +71,69 @@ module Danger
|
|
41
71
|
end
|
42
72
|
|
43
73
|
def table(name, emoji, violations, all_previous_violations)
|
44
|
-
content = violations.map { |v| process_markdown(v) }
|
74
|
+
content = violations.map { |v| process_markdown(v) }
|
75
|
+
|
45
76
|
kind = table_kind_from_title(name)
|
46
77
|
previous_violations = all_previous_violations[kind] || []
|
47
|
-
|
48
|
-
|
78
|
+
resolved_violations = previous_violations.reject do |pv|
|
79
|
+
content.count { |v| messages_are_equivalent(v, pv) } > 0
|
80
|
+
end
|
81
|
+
|
82
|
+
resolved_messages = resolved_violations.map(&:message).uniq
|
49
83
|
count = content.count
|
50
84
|
|
51
85
|
{
|
52
86
|
name: name,
|
53
87
|
emoji: emoji,
|
54
88
|
content: content,
|
55
|
-
resolved:
|
89
|
+
resolved: resolved_messages,
|
56
90
|
count: count
|
57
91
|
}
|
58
92
|
end
|
59
93
|
|
60
|
-
def
|
61
|
-
if title =~ /error/i
|
62
|
-
:error
|
63
|
-
elsif title =~ /warning/i
|
64
|
-
:warning
|
65
|
-
elsif title =~ /message/i
|
66
|
-
:message
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
def generate_comment(warnings: [], errors: [], messages: [], markdowns: [], previous_violations: {}, danger_id: "danger", template: "github")
|
94
|
+
def apply_template(tables: [], markdowns: [], danger_id: "danger", template: "github")
|
71
95
|
require "erb"
|
72
96
|
|
73
97
|
md_template = File.join(Danger.gem_path, "lib/danger/comment_generators/#{template}.md.erb")
|
74
98
|
|
75
99
|
# erb: http://www.rrn.dk/rubys-erb-templating-system
|
76
100
|
# for the extra args: http://stackoverflow.com/questions/4632879/erb-template-removing-the-trailing-line
|
77
|
-
@tables =
|
78
|
-
|
79
|
-
table("Warning", "warning", warnings, previous_violations),
|
80
|
-
table("Message", "book", messages, previous_violations)
|
81
|
-
]
|
82
|
-
@markdowns = markdowns
|
101
|
+
@tables = tables
|
102
|
+
@markdowns = markdowns.map(&:message)
|
83
103
|
@danger_id = danger_id
|
84
104
|
|
85
105
|
return ERB.new(File.read(md_template), 0, "-").result(binding)
|
86
106
|
end
|
87
107
|
|
108
|
+
def generate_comment(warnings: [], errors: [], messages: [], markdowns: [], previous_violations: {}, danger_id: "danger", template: "github")
|
109
|
+
apply_template(
|
110
|
+
tables: [
|
111
|
+
table("Error", "no_entry_sign", errors, previous_violations),
|
112
|
+
table("Warning", "warning", warnings, previous_violations),
|
113
|
+
table("Message", "book", messages, previous_violations)
|
114
|
+
],
|
115
|
+
markdowns: markdowns,
|
116
|
+
danger_id: danger_id,
|
117
|
+
template: template
|
118
|
+
)
|
119
|
+
end
|
120
|
+
|
121
|
+
def generate_inline_comment_body(emoji, message, danger_id: "danger", resolved: false, template: "github")
|
122
|
+
apply_template(
|
123
|
+
tables: [{ content: [message], resolved: resolved, emoji: emoji }],
|
124
|
+
danger_id: danger_id,
|
125
|
+
template: "#{template}_inline"
|
126
|
+
)
|
127
|
+
end
|
128
|
+
|
129
|
+
def generate_inline_markdown_body(markdown, danger_id: "danger", template: "github")
|
130
|
+
apply_template(
|
131
|
+
markdowns: [markdown],
|
132
|
+
danger_id: danger_id,
|
133
|
+
template: "#{template}_inline"
|
134
|
+
)
|
135
|
+
end
|
136
|
+
|
88
137
|
def generate_description(warnings: nil, errors: nil)
|
89
138
|
if errors.empty? && warnings.empty?
|
90
139
|
return "All green. #{random_compliment}"
|
@@ -98,9 +147,21 @@ module Danger
|
|
98
147
|
end
|
99
148
|
|
100
149
|
def random_compliment
|
101
|
-
|
102
|
-
|
103
|
-
|
150
|
+
["Well done.", "Congrats.", "Woo!",
|
151
|
+
"Yay.", "Jolly good show.", "Good on 'ya.", "Nice work."].sample
|
152
|
+
end
|
153
|
+
|
154
|
+
def character_from_emoji(emoji)
|
155
|
+
emoji.delete! ":"
|
156
|
+
if emoji == "no_entry_sign"
|
157
|
+
"🚫"
|
158
|
+
elsif emoji == "warning"
|
159
|
+
"⚠️"
|
160
|
+
elsif emoji == "book"
|
161
|
+
"📖"
|
162
|
+
elsif emoji == "white_check_mark"
|
163
|
+
"✅"
|
164
|
+
end
|
104
165
|
end
|
105
166
|
|
106
167
|
private
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Danger
|
2
|
+
module Helpers
|
3
|
+
module CommentsParsingHelper
|
4
|
+
# !@group Extension points
|
5
|
+
# Produces a message-like from a row in a comment table
|
6
|
+
#
|
7
|
+
# @param [String] row
|
8
|
+
# The content of the row in the table
|
9
|
+
#
|
10
|
+
# @return [Violation or Markdown] the extracted message
|
11
|
+
def parse_message_from_row(row)
|
12
|
+
Violation.new(row, true, nil, nil)
|
13
|
+
end
|
14
|
+
|
15
|
+
def parse_tables_from_comment(comment)
|
16
|
+
comment.split("</table>")
|
17
|
+
end
|
18
|
+
|
19
|
+
def violations_from_table(table)
|
20
|
+
row_regex = %r{<td data-sticky="true">(?:<del>)?(.*?)(?:</del>)?\s*</td>}im
|
21
|
+
table.scan(row_regex).flatten.map do |row|
|
22
|
+
parse_message_from_row(row.strip)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def parse_comment(comment)
|
27
|
+
tables = parse_tables_from_comment(comment)
|
28
|
+
violations = {}
|
29
|
+
tables.each do |table|
|
30
|
+
next unless table =~ %r{<th width="100%"(.*?)</th>}im
|
31
|
+
title = Regexp.last_match(1)
|
32
|
+
kind = table_kind_from_title(title)
|
33
|
+
next unless kind
|
34
|
+
|
35
|
+
violations[kind] = violations_from_table(table)
|
36
|
+
end
|
37
|
+
|
38
|
+
violations.reject { |_, v| v.empty? }
|
39
|
+
end
|
40
|
+
|
41
|
+
def table_kind_from_title(title)
|
42
|
+
if title =~ /error/i
|
43
|
+
:error
|
44
|
+
elsif title =~ /warning/i
|
45
|
+
:warning
|
46
|
+
elsif title =~ /message/i
|
47
|
+
:message
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
require "danger/helpers/comments_helper"
|
3
|
+
|
4
|
+
module Danger
|
5
|
+
module RequestSources
|
6
|
+
class BitbucketServer < RequestSource
|
7
|
+
include Danger::Helpers::CommentsHelper
|
8
|
+
attr_accessor :pr_json
|
9
|
+
|
10
|
+
def initialize(ci_source, environment)
|
11
|
+
self.ci_source = ci_source
|
12
|
+
self.environment = environment
|
13
|
+
|
14
|
+
project, slug = ci_source.repo_slug.split("/")
|
15
|
+
@api = BitbucketServerAPI.new(project, slug, ci_source.pull_request_id, environment)
|
16
|
+
end
|
17
|
+
|
18
|
+
def validates_as_ci?
|
19
|
+
# TODO: ???
|
20
|
+
true
|
21
|
+
end
|
22
|
+
|
23
|
+
def validates_as_api_source?
|
24
|
+
@api.credentials_given?
|
25
|
+
end
|
26
|
+
|
27
|
+
def scm
|
28
|
+
@scm ||= GitRepo.new
|
29
|
+
end
|
30
|
+
|
31
|
+
def host
|
32
|
+
@host ||= @api.host
|
33
|
+
end
|
34
|
+
|
35
|
+
def fetch_details
|
36
|
+
self.pr_json = @api.fetch_pr_json
|
37
|
+
end
|
38
|
+
|
39
|
+
def setup_danger_branches
|
40
|
+
base_commit = self.pr_json[:toRef][:latestCommit]
|
41
|
+
head_commit = self.pr_json[:fromRef][:latestCommit]
|
42
|
+
|
43
|
+
# Next, we want to ensure that we have a version of the current branch at a known location
|
44
|
+
self.scm.exec "branch #{EnvironmentManager.danger_base_branch} #{base_commit}"
|
45
|
+
|
46
|
+
# OK, so we want to ensure that we have a known head branch, this will always represent
|
47
|
+
# the head of the PR ( e.g. the most recent commit that will be merged. )
|
48
|
+
self.scm.exec "branch #{EnvironmentManager.danger_head_branch} #{head_commit}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def organisation
|
52
|
+
nil
|
53
|
+
end
|
54
|
+
|
55
|
+
def update_pull_request!(warnings: [], errors: [], messages: [], markdowns: [], danger_id: "danger")
|
56
|
+
delete_old_comments(danger_id: danger_id)
|
57
|
+
|
58
|
+
comment = generate_description(warnings: warnings, errors: errors)
|
59
|
+
comment += "\n\n"
|
60
|
+
comment += generate_comment(warnings: warnings,
|
61
|
+
errors: errors,
|
62
|
+
messages: messages,
|
63
|
+
markdowns: markdowns,
|
64
|
+
previous_violations: {},
|
65
|
+
danger_id: danger_id,
|
66
|
+
template: "bitbucket_server")
|
67
|
+
|
68
|
+
@api.post_comment(comment)
|
69
|
+
end
|
70
|
+
|
71
|
+
def delete_old_comments(danger_id: "danger")
|
72
|
+
@api.fetch_last_comments.each do |c|
|
73
|
+
@api.delete_comment(c[:id], c[:version]) if c[:text] =~ /generated_by_#{danger_id}/
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
require "danger/helpers/comments_helper"
|
3
|
+
|
4
|
+
module Danger
|
5
|
+
module RequestSources
|
6
|
+
class BitbucketServerAPI
|
7
|
+
attr_accessor :host, :pr_api_endpoint
|
8
|
+
|
9
|
+
def initialize(project, slug, pull_request_id, environment)
|
10
|
+
@username = environment["DANGER_BITBUCKETSERVER_USERNAME"]
|
11
|
+
@password = environment["DANGER_BITBUCKETSERVER_PASSWORD"]
|
12
|
+
self.host = environment["DANGER_BITBUCKETSERVER_HOST"]
|
13
|
+
self.pr_api_endpoint = "https://#{host}/rest/api/1.0/projects/#{project}/repos/#{slug}/pull-requests/#{pull_request_id}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def credentials_given?
|
17
|
+
@username && !@username.empty? && @password && !@password.empty?
|
18
|
+
end
|
19
|
+
|
20
|
+
def fetch_pr_json
|
21
|
+
uri = URI(pr_api_endpoint)
|
22
|
+
fetch_json(uri)
|
23
|
+
end
|
24
|
+
|
25
|
+
def fetch_last_comments
|
26
|
+
uri = URI("#{pr_api_endpoint}/activities?limit=1000")
|
27
|
+
fetch_json(uri)[:values].select { |v| v[:action] == "COMMENTED" }.map { |v| v[:comment] }
|
28
|
+
end
|
29
|
+
|
30
|
+
def delete_comment(id, version)
|
31
|
+
uri = URI("#{pr_api_endpoint}/comments/#{id}?version=#{version}")
|
32
|
+
delete(uri)
|
33
|
+
end
|
34
|
+
|
35
|
+
def post_comment(text)
|
36
|
+
uri = URI("#{pr_api_endpoint}/comments")
|
37
|
+
body = { text: text }.to_json
|
38
|
+
post(uri, body)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def fetch_json(uri)
|
44
|
+
req = Net::HTTP::Get.new(uri.request_uri, { "Content-Type" => "application/json" })
|
45
|
+
req.basic_auth @username, @password
|
46
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
|
47
|
+
http.request(req)
|
48
|
+
end
|
49
|
+
JSON.parse(res.body, symbolize_names: true)
|
50
|
+
end
|
51
|
+
|
52
|
+
def post(uri, body)
|
53
|
+
req = Net::HTTP::Post.new(uri.request_uri, { "Content-Type" => "application/json" })
|
54
|
+
req.basic_auth @username, @password
|
55
|
+
req.body = body
|
56
|
+
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
|
57
|
+
http.request(req)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def delete(uri)
|
62
|
+
req = Net::HTTP::Delete.new(uri.request_uri, { "Content-Type" => "application/json" })
|
63
|
+
req.basic_auth @username, @password
|
64
|
+
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
|
65
|
+
http.request(req)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|