danger 3.1.1 → 3.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- m << ". Updating the Danger gem might fix the issue.\n"
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
- def parse_tables_from_comment(comment)
11
- comment.split("</table>")
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
- def violations_from_table(table)
15
- regex = %r{<td data-sticky="true">(?:<del>)?(.*?)(?:</del>)?\s*</td>}im
16
- table.scan(regex).flatten.map(&:strip)
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
- html = markdown_parser(violation.message).to_html
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) }.uniq
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
- messages = content.map(&:message)
48
- resolved_violations = previous_violations.uniq - messages
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: resolved_violations,
89
+ resolved: resolved_messages,
56
90
  count: count
57
91
  }
58
92
  end
59
93
 
60
- def table_kind_from_title(title)
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
- table("Error", "no_entry_sign", errors, previous_violations),
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
- compliment = ["Well done.", "Congrats.", "Woo!",
102
- "Yay.", "Jolly good show.", "Good on 'ya.", "Nice work."]
103
- compliment.sample
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