danger 3.1.1 → 3.2.0

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.
@@ -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