danger 0.5.1 → 0.7.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.
@@ -7,7 +7,7 @@ module Danger
7
7
  class Dangerfile
8
8
  include Danger::Dangerfile::DSL
9
9
 
10
- attr_accessor :env, :warnings, :errors, :messages, :verbose
10
+ attr_accessor :env, :warnings, :errors, :messages, :markdowns, :verbose
11
11
 
12
12
  # @return [Pathname] the path where the Dangerfile was loaded from. It is nil
13
13
  # if the Dangerfile was generated programmatically.
@@ -26,8 +26,8 @@ module Danger
26
26
  rows = []
27
27
 
28
28
  AvailableValues.all.each do |key|
29
- next if key == :pr_body
30
29
  value = self.send(key)
30
+ value = value.scan(/.{,80}/).to_a.each(&:strip!).join("\n") if key == :pr_body
31
31
 
32
32
  # So that we either have one value per row
33
33
  # or we have [] for an empty array
@@ -40,8 +40,8 @@ module Danger
40
40
  rows << ["SCM", env.scm.class]
41
41
  rows << ["Source", env.ci_source.class]
42
42
  rows << ["Requests", env.request_source.class]
43
- rows << ["Base Commit", env.request_source.base_commit]
44
- rows << ["Head Commit", env.request_source.head_commit]
43
+ rows << ["Base Commit", env.meta_info_for_base]
44
+ rows << ["Head Commit", env.meta_info_for_head]
45
45
 
46
46
  params = {}
47
47
  params[:rows] = rows.each { |current| current[0] = current[0].yellow }
@@ -49,8 +49,6 @@ module Danger
49
49
 
50
50
  puts ""
51
51
  puts Terminal::Table.new(params)
52
- puts "PR Body:"
53
- puts self.pr_body.cyan
54
52
  puts ""
55
53
  end
56
54
 
@@ -92,5 +90,34 @@ module Danger
92
90
  # rubocop:enable Lint/RescueException
93
91
  end
94
92
  end
93
+
94
+ def print_results
95
+ return if (self.errors + self.warnings + self.messages + self.markdowns).count == 0
96
+
97
+ puts ""
98
+ puts "danger results:"
99
+ [:errors, :warnings, :messages].each do |current|
100
+ params = {}
101
+ params[:rows] = self.send(current).collect { |a| [a.message] }
102
+ next unless params[:rows].count > 0
103
+ params[:title] = case current
104
+ when :errors
105
+ current.to_s.capitalize.red
106
+ when :warnings
107
+ current.to_s.capitalize.yellow
108
+ else
109
+ current.to_s.capitalize
110
+ end
111
+
112
+ puts ""
113
+ puts Terminal::Table.new(params)
114
+ puts ""
115
+ end
116
+
117
+ puts "Markdown: ".green if self.markdowns.count > 0
118
+ self.markdowns.each do |current_markdown|
119
+ puts current_markdown
120
+ end
121
+ end
95
122
  end
96
123
  end
@@ -1,3 +1,5 @@
1
+ require 'danger/violation'
2
+
1
3
  module Danger
2
4
  class Dangerfile
3
5
  module DSL
@@ -13,14 +15,63 @@ module Danger
13
15
  self.warnings = []
14
16
  self.errors = []
15
17
  self.messages = []
18
+ self.markdowns = []
19
+
20
+ load_default_plugins
21
+ end
22
+
23
+ def import(path)
24
+ raise "`import` requires a string" unless path.kind_of?(String)
25
+ path += ".rb" unless path.end_with?(".rb")
26
+
27
+ if path.start_with?("http")
28
+ import_url(path)
29
+ else
30
+ import_local(path)
31
+ end
32
+ end
33
+
34
+ # Download a remote plugin and use it locally
35
+ #
36
+ # @param [String] url
37
+ # https URL to the Ruby file to use
38
+ def import_url(url)
39
+ raise "URL is not https, for security reasons `danger` only supports encrypted requests" unless url.start_with?("https://")
40
+
41
+ require 'tmpdir'
42
+ require 'faraday'
43
+ content = Faraday.get(url)
44
+ Dir.mktmpdir do |dir|
45
+ path = File.join(dir, "temporary_remote_action.rb")
46
+ File.write(path, content.body)
47
+ import_local(path)
48
+ end
49
+ end
50
+
51
+ # Import one or more local plugins
52
+ #
53
+ # @param [String] path
54
+ # The path to the file to import
55
+ # Can also be a pattern (./**/*plugin.rb)
56
+ def import_local(path)
57
+ Dir[path].each do |file|
58
+ require File.expand_path(file) # without the expand_path it would fail if the path doesn't start with ./
59
+ end
60
+ end
61
+
62
+ def should_ignore_violation(message)
63
+ env.request_source.ignored_violations.include? message
16
64
  end
17
65
 
18
66
  # Declares a CI blocking error
19
67
  #
20
68
  # @param [String] message
21
69
  # The message to present to the user
22
- def fail(message)
23
- self.errors << message
70
+ # @param [Boolean] sticky
71
+ # Whether the message should be kept after it was fixed
72
+ def fail(message, sticky: true)
73
+ return if should_ignore_violation(message)
74
+ self.errors << Violation.new(message, sticky)
24
75
  puts "Raising error '#{message}'"
25
76
  end
26
77
 
@@ -28,8 +79,11 @@ module Danger
28
79
  #
29
80
  # @param [String] message
30
81
  # The message to present to the user
31
- def warn(message)
32
- self.warnings << message
82
+ # @param [Boolean] sticky
83
+ # Whether the message should be kept after it was fixed
84
+ def warn(message, sticky: true)
85
+ return if should_ignore_violation(message)
86
+ self.warnings << Violation.new(message, sticky)
33
87
  puts "Printing warning '#{message}'"
34
88
  end
35
89
 
@@ -37,28 +91,61 @@ module Danger
37
91
  #
38
92
  # @param [String] message
39
93
  # The message to present to the user
40
- def message(message)
41
- self.messages << message
94
+ # @param [Boolean] sticky
95
+ # Whether the message should be kept after it was fixed
96
+ def message(message, sticky: true)
97
+ self.messages << Violation.new(message, sticky)
42
98
  puts "Printing message '#{message}'"
43
99
  end
44
100
 
101
+ # Print markdown to below the table
102
+ #
103
+ # @param [String] message
104
+ # The markdown based message to be printed below the table
105
+ def markdown(message)
106
+ self.markdowns << message
107
+ puts "Printing markdown #{message}"
108
+ end
109
+
45
110
  # When an undefined method is called, we check to see if it's something
46
111
  # that either the `scm` or the `request_source` can handle.
47
112
  # This opens us up to letting those object extend themselves naturally.
48
- def method_missing(method_sym, *_arguments, &_block)
49
- unless AvailableValues.all.include?(method_sym)
50
- raise "Unknown method '#{method_sym}', please check out the documentation for available variables".red
51
- end
52
-
113
+ # This will also look for plugins
114
+ def method_missing(method_sym, *arguments, &_block)
115
+ # SCM Source
53
116
  if AvailableValues.scm.include?(method_sym)
54
- # SCM Source
55
117
  return env.scm.send(method_sym)
56
118
  end
57
119
 
120
+ # Request Source
58
121
  if AvailableValues.request_source.include?(method_sym)
59
- # Request Source
60
122
  return env.request_source.send(method_sym)
61
123
  end
124
+
125
+ # Plugins
126
+ class_name = method_sym.to_s.danger_class
127
+ if Danger::Dangerfile::DSL.const_defined?(class_name)
128
+ plugin_ref = Danger::Dangerfile::DSL.const_get(class_name)
129
+ if plugin_ref < Plugin
130
+ plugin_ref.new(self).run(*arguments)
131
+ else
132
+ raise "'#{method_sym}' is not a valid danger plugin".red
133
+ end
134
+ else
135
+ raise "Unknown method '#{method_sym}', please check out the documentation for available plugins".red
136
+ end
137
+ end
138
+
139
+ private
140
+
141
+ def load_default_plugins
142
+ Dir["./lib/danger/plugins/*.rb"].each do |file|
143
+ require File.expand_path(file)
144
+ end
145
+
146
+ Dir["./danger_plugins/*.rb"].each do |file|
147
+ require File.expand_path(file)
148
+ end
62
149
  end
63
150
  end
64
151
  end
@@ -32,5 +32,40 @@ module Danger
32
32
 
33
33
  self.scm = GitRepo.new # For now
34
34
  end
35
+
36
+ def ensure_danger_branches_are_setup
37
+ # As this currently just works with GitHub, we can use a github specific feature here:
38
+ pull_id = ci_source.pull_request_id
39
+ test_branch = request_source.base_commit
40
+
41
+ # Next, we want to ensure that we have a version of the current branch that at a know location
42
+ scm.exec "branch #{danger_base_branch} #{test_branch}"
43
+
44
+ # OK, so we want to ensure that we have a known head branch, this will always represent
45
+ # the head ( e.g. the most recent commit that will be merged. )
46
+ scm.exec "fetch origin +refs/pull/#{pull_id}/merge:#{danger_head_branch}"
47
+ end
48
+
49
+ def clean_up
50
+ [danger_base_branch, danger_base_branch].each do |branch|
51
+ scm.exec "branch -d #{branch}"
52
+ end
53
+ end
54
+
55
+ def meta_info_for_base
56
+ scm.exec("--no-pager log #{danger_base_branch} -n1")
57
+ end
58
+
59
+ def meta_info_for_head
60
+ scm.exec("--no-pager log #{danger_head_branch} -n1")
61
+ end
62
+
63
+ def danger_head_branch
64
+ "danger_head"
65
+ end
66
+
67
+ def danger_base_branch
68
+ "danger_base"
69
+ end
35
70
  end
36
71
  end
@@ -0,0 +1,26 @@
1
+ module Danger
2
+ class Dangerfile
3
+ module DSL
4
+ class Plugin
5
+ def initialize(dsl)
6
+ @dsl = dsl
7
+ end
8
+
9
+ # Since we have a reference to the DSL containing all the information
10
+ # We need to redirect the self calls to the DSL
11
+ def method_missing(method_sym, *arguments, &_block)
12
+ return @dsl.send(method_sym, *arguments) if @dsl.respond_to?(method_sym)
13
+ return @dsl.method_missing(method_sym, *arguments)
14
+ end
15
+
16
+ def run
17
+ raise "run method must be implemented"
18
+ end
19
+
20
+ def self.description
21
+ "Add plugin description here"
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,34 @@
1
+ module Danger
2
+ class Dangerfile
3
+ module DSL
4
+ class ProtectFiles < Plugin
5
+ def run(path: nil, message: nil, fail_build: true)
6
+ raise "You have to provide a message" if message.to_s.length == 0
7
+ raise "You have to provide a path" if path.to_s.length == 0
8
+
9
+ broken_rule = false
10
+
11
+ Dir.glob(path) do |current|
12
+ broken_rule = true if self.env.scm.modified_files.include?(current)
13
+ end
14
+
15
+ return unless broken_rule
16
+
17
+ if fail_build
18
+ @dsl.errors << message
19
+ else
20
+ @dsl.warnings << message
21
+ end
22
+ end
23
+
24
+ def self.description
25
+ [
26
+ "Protect a file from being changed. This can",
27
+ "be used in combination with some kind of",
28
+ "permission check if a user is inside the org"
29
+ ].join(" ")
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -4,7 +4,7 @@ require 'redcarpet'
4
4
 
5
5
  module Danger
6
6
  class GitHub
7
- attr_accessor :ci_source, :pr_json, :issue_json, :environment, :base_commit, :head_commit, :support_tokenless_auth
7
+ attr_accessor :ci_source, :pr_json, :issue_json, :environment, :base_commit, :head_commit, :support_tokenless_auth, :ignored_violations, :github_host
8
8
 
9
9
  def initialize(ci_source, environment)
10
10
  self.ci_source = ci_source
@@ -12,24 +12,35 @@ module Danger
12
12
  self.support_tokenless_auth = false
13
13
 
14
14
  Octokit.auto_paginate = true
15
+ @token = @environment["DANGER_GITHUB_API_TOKEN"]
16
+ self.github_host = @environment["DANGER_GITHUB_HOST"] || "github.com"
17
+ if @environment["DANGER_GITHUB_API_HOST"]
18
+ Octokit.api_endpoint = @environment["DANGER_GITHUB_API_HOST"]
19
+ end
15
20
  end
16
21
 
17
22
  def client
18
- token = @environment["DANGER_GITHUB_API_TOKEN"]
19
- raise "No API given, please provide one using `DANGER_GITHUB_API_TOKEN`" if !token && !support_tokenless_auth
23
+ raise "No API given, please provide one using `DANGER_GITHUB_API_TOKEN`" if !@token && !support_tokenless_auth
20
24
 
21
25
  @client ||= Octokit::Client.new(
22
- access_token: token
26
+ access_token: @token
23
27
  )
24
28
  end
25
29
 
26
30
  def markdown_parser
27
- @markdown_parser ||= Redcarpet::Markdown.new(Redcarpet::Render::HTML)
31
+ @markdown_parser ||= Redcarpet::Markdown.new(Redcarpet::Render::HTML, no_intra_emphasis: true)
28
32
  end
29
33
 
30
34
  def fetch_details
31
35
  self.pr_json = client.pull_request(ci_source.repo_slug, ci_source.pull_request_id)
32
36
  fetch_issue_details(self.pr_json)
37
+ self.ignored_violations = ignored_violations_from_pr(self.pr_json)
38
+ end
39
+
40
+ def ignored_violations_from_pr(pr_json)
41
+ pr_body = pr_json[:body]
42
+ return [] if pr_body.nil?
43
+ pr_body.chomp.scan(/>\s*danger\s*:\s*ignore\s*"(.*)"/i).flatten
33
44
  end
34
45
 
35
46
  def fetch_issue_details(pr_json)
@@ -45,16 +56,20 @@ module Danger
45
56
  self.pr_json[:head][:sha]
46
57
  end
47
58
 
59
+ def branch_for_merge
60
+ self.pr_json[:base][:ref]
61
+ end
62
+
48
63
  def pr_title
49
- self.pr_json[:title]
64
+ self.pr_json[:title].to_s
50
65
  end
51
66
 
52
67
  def pr_body
53
- self.pr_json[:body]
68
+ self.pr_json[:body].to_s
54
69
  end
55
70
 
56
71
  def pr_author
57
- self.pr_json[:user][:login]
72
+ self.pr_json[:user][:login].to_s
58
73
  end
59
74
 
60
75
  def pr_labels
@@ -62,16 +77,28 @@ module Danger
62
77
  end
63
78
 
64
79
  # Sending data to GitHub
65
- def update_pull_request!(warnings: nil, errors: nil, messages: nil)
80
+ def update_pull_request!(warnings: [], errors: [], messages: [], markdowns: [])
66
81
  comment_result = {}
67
82
 
68
- if (warnings + errors + messages).empty?
83
+ issues = client.issue_comments(ci_source.repo_slug, ci_source.pull_request_id)
84
+ editable_issues = issues.reject { |issue| issue[:body].include?("generated_by_danger") == false }
85
+
86
+ if editable_issues.empty?
87
+ previous_violations = {}
88
+ else
89
+ comment = editable_issues.first[:body]
90
+ previous_violations = parse_comment(comment)
91
+ end
92
+
93
+ if previous_violations.empty? && (warnings + errors + messages + markdowns).empty?
69
94
  # Just remove the comment, if there's nothing to say.
70
95
  delete_old_comments!
71
96
  else
72
- issues = client.issue_comments(ci_source.repo_slug, ci_source.pull_request_id)
73
- editable_issues = issues.reject { |issue| issue[:body].include?("generated_by_danger") == false }
74
- body = generate_comment(warnings: warnings, errors: errors, messages: messages)
97
+ body = generate_comment(warnings: warnings,
98
+ errors: errors,
99
+ messages: messages,
100
+ markdowns: markdowns,
101
+ previous_violations: previous_violations)
75
102
 
76
103
  if editable_issues.empty?
77
104
  comment_result = client.add_comment(ci_source.repo_slug, ci_source.pull_request_id, body)
@@ -93,7 +120,7 @@ module Danger
93
120
  message = generate_github_description(warnings: warnings, errors: errors)
94
121
  client.create_status(ci_source.repo_slug, latest_pr_commit_ref, status, {
95
122
  description: message,
96
- context: "KrauseFx/danger",
123
+ context: "danger/danger",
97
124
  target_url: details_url
98
125
  })
99
126
  rescue
@@ -118,11 +145,15 @@ module Danger
118
145
  end
119
146
  end
120
147
 
148
+ def random_compliment
149
+ compliment = ["Well done.", "Congrats.", "Woo!",
150
+ "Yay.", "Jolly good show.", "Good on 'ya.", "Nice work."]
151
+ compliment.sample
152
+ end
153
+
121
154
  def generate_github_description(warnings: nil, errors: nil)
122
155
  if errors.empty? && warnings.empty?
123
- compliment = ["Well done.", "Congrats.", "Woo!",
124
- "Yay.", "Jolly good show.", "Good on 'ya.", "Nice work."]
125
- return "All green. #{compliment.sample}"
156
+ return "All green. #{random_compliment}"
126
157
  else
127
158
  message = "⚠ "
128
159
  message += "#{errors.count} Error#{errors.count == 1 ? '' : 's'}. " unless errors.empty?
@@ -132,7 +163,7 @@ module Danger
132
163
  end
133
164
  end
134
165
 
135
- def generate_comment(warnings: [], errors: [], messages: [])
166
+ def generate_comment(warnings: [], errors: [], messages: [], markdowns: [], previous_violations: {})
136
167
  require 'erb'
137
168
 
138
169
  md_template = File.join(Danger.gem_path, "lib/danger/comment_generators/github.md.erb")
@@ -140,18 +171,64 @@ module Danger
140
171
  # erb: http://www.rrn.dk/rubys-erb-templating-system
141
172
  # for the extra args: http://stackoverflow.com/questions/4632879/erb-template-removing-the-trailing-line
142
173
  @tables = [
143
- { name: "Error", emoji: "no_entry_sign", content: errors.map { |s| process_markdown(s) } },
144
- { name: "Warning", emoji: "warning", content: warnings.map { |s| process_markdown(s) } },
145
- { name: "Message", emoji: "book", content: messages.map { |s| process_markdown(s) } }
174
+ table("Error", "no_entry_sign", errors, previous_violations),
175
+ table("Warning", "warning", warnings, previous_violations),
176
+ table("Message", "book", messages, previous_violations)
146
177
  ]
178
+ @markdowns = markdowns
179
+
147
180
  return ERB.new(File.read(md_template), 0, "-").result(binding)
148
181
  end
149
182
 
150
- def process_markdown(string)
151
- html = markdown_parser.render(string)
183
+ def table(name, emoji, violations, all_previous_violations)
184
+ content = violations.map { |v| process_markdown(v) }
185
+ kind = table_kind_from_title(name)
186
+ previous_violations = all_previous_violations[kind] || []
187
+ messages = content.map(&:message)
188
+ resolved_violations = previous_violations.reject { |s| messages.include? s }
189
+ count = content.count
190
+ { name: name, emoji: emoji, content: content, resolved: resolved_violations, count: count }
191
+ end
192
+
193
+ def parse_comment(comment)
194
+ tables = parse_tables_from_comment(comment)
195
+ violations = {}
196
+ tables.each do |table|
197
+ next unless table =~ %r{<th width="100%"(.*?)</th>}im
198
+ title = Regexp.last_match(1)
199
+ kind = table_kind_from_title(title)
200
+ next unless kind
201
+
202
+ violations[kind] = violations_from_table(table)
203
+ end
204
+
205
+ violations.reject { |_, v| v.empty? }
206
+ end
207
+
208
+ def violations_from_table(table)
209
+ regex = %r{<td data-sticky="true">(?:<del>)?(.*?)(?:</del>)?\s*</td>}im
210
+ table.scan(regex).flatten.map(&:strip)
211
+ end
212
+
213
+ def table_kind_from_title(title)
214
+ if title =~ /error/i
215
+ :error
216
+ elsif title =~ /warning/i
217
+ :warning
218
+ elsif title =~ /message/i
219
+ :message
220
+ end
221
+ end
222
+
223
+ def parse_tables_from_comment(comment)
224
+ comment.split('</table>')
225
+ end
226
+
227
+ def process_markdown(violation)
228
+ html = markdown_parser.render(violation.message)
152
229
  match = html.match(%r{^<p>(.*)</p>$})
153
- return match.captures.first unless match.nil?
154
- html
230
+ message = match.nil? ? html : match.captures.first
231
+ Violation.new(message, violation.sticky)
155
232
  end
156
233
  end
157
234
  end
@@ -0,0 +1,12 @@
1
+ module Danger
2
+ class FileList < Array
3
+ # Information about pattern: http://ruby-doc.org/core-2.2.0/File.html#method-c-fnmatch
4
+ # e.g. "**/something.*" for any file called something with any extension
5
+ def include?(pattern)
6
+ self.each do |current|
7
+ return true if File.fnmatch(pattern, current)
8
+ end
9
+ return false
10
+ end
11
+ end
12
+ end
@@ -1,38 +1,42 @@
1
1
  # For more info see: https://github.com/schacon/ruby-git
2
2
 
3
- require 'grit'
3
+ require 'git'
4
4
 
5
5
  module Danger
6
6
  class GitRepo
7
7
  attr_accessor :diff
8
8
 
9
9
  def diff_for_folder(folder, from: "master", to: 'HEAD')
10
- repo = Grit::Repo.new folder
10
+ repo = Git.open folder
11
11
  self.diff = repo.diff(from, to)
12
12
  end
13
13
 
14
- def files_added
15
- @diff.select(&:new_file).map(&:b_path)
14
+ def exec(string)
15
+ `git #{string}`.strip
16
16
  end
17
17
 
18
- def files_deleted
19
- @diff.select(&:deleted_file).map(&:a_path)
18
+ def added_files
19
+ Danger::FileList.new(@diff.select { |diff| diff.type == "new" }.map(&:path))
20
20
  end
21
21
 
22
- def files_modified
23
- @diff.reject(&:deleted_file).reject(&:new_file).map(&:a_path)
22
+ def deleted_files
23
+ Danger::FileList.new(@diff.select { |diff| diff.type == "deleted" }.map(&:path))
24
+ end
25
+
26
+ def modified_files
27
+ Danger::FileList.new(@diff.stats[:files].keys)
24
28
  end
25
29
 
26
30
  def lines_of_code
27
- @diff.map(&:diff).map(&:lines).flatten.count { |l| l.start_with?("+") || l.start_with?("-") }
31
+ @diff.lines
28
32
  end
29
33
 
30
34
  def deletions
31
- @diff.map(&:diff).map(&:lines).flatten.count { |l| l.start_with?("-") }
35
+ @diff.deletions
32
36
  end
33
37
 
34
38
  def insertions
35
- @diff.map(&:diff).map(&:lines).flatten.count { |l| l.start_with?("+") }
39
+ @diff.insertions
36
40
  end
37
41
  end
38
42
  end
@@ -79,7 +79,7 @@ module Danger
79
79
  lines = contents.lines
80
80
  indent = ' # '
81
81
  indicator = indent.tr('#', '>')
82
- first_line = (line_numer.zero?)
82
+ first_line = line_numer.zero?
83
83
  last_line = (line_numer == (lines.count - 1))
84
84
 
85
85
  m << "\n"
@@ -1,4 +1,4 @@
1
1
  module Danger
2
- VERSION = "0.5.1"
3
- DESCRIPTION = "Automate your PR etiquette."
2
+ VERSION = "0.7.0".freeze
3
+ DESCRIPTION = "Automate your PR etiquette.".freeze
4
4
  end
@@ -0,0 +1,10 @@
1
+ module Danger
2
+ class Violation
3
+ attr_accessor :message, :sticky
4
+
5
+ def initialize(message, sticky)
6
+ self.message = message
7
+ self.sticky = sticky
8
+ end
9
+ end
10
+ end
data/lib/danger.rb CHANGED
@@ -3,6 +3,7 @@ require "danger/dangerfile"
3
3
  require "danger/environment_manager"
4
4
  require "danger/commands/runner"
5
5
  require "danger/available_values"
6
+ require "danger/core_ext/string"
6
7
 
7
8
  require "claide"
8
9
  require "colored"