danger 0.6.0 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8bf96232696dca35c69a7d338d10bf16af6917a2
4
- data.tar.gz: e5e7f69f74d8419ce1c0f2c0df2725aa8b1874bf
3
+ metadata.gz: f405c58b164cfb051f33ce62eff96d6954ccedf8
4
+ data.tar.gz: e487a8349af60005ce0f63ec429cd8e73a561055
5
5
  SHA512:
6
- metadata.gz: c69c8d1096130a36a0749c55a6f1e7e90d2df55ee0048bfdd13d13f83760071e66c7a65ad188b3c12a4d71613f75d982698e3e7776438bc1dc3ec227a94fd445
7
- data.tar.gz: 16a07e94b369b7da16ab0bab5c4d9935c40442a3284c43985254811f92da77439d81dc8c19dbb6f4a9893f1ea89fcac6155736f9dd07f9b000b1457c1d2d31ad
6
+ metadata.gz: 3ce00623811798d3bf0cb0e77bc788bc0c99fac1045d00a33cf3c68b297983e669de41544bae3f8310257e3a34a3e8704aedb2d9f9fc2064e4f0a3e443cdcbc5
7
+ data.tar.gz: e0cdd1d536430c45226a3638ff0a2cd659db62a19e643c931230961e00967e7705321ec561c2afd276c0e73f6570d2335f7126b82017be95f5dcd0de18d9c906
data/README.md CHANGED
@@ -38,7 +38,7 @@ bundle exec danger init
38
38
  bundle exec danger
39
39
  ```
40
40
 
41
- This will look at your `Dangerfile` and update the pull request accordingly. While you are setting up Danger, you may want to use: `--verbose` for more debug information.
41
+ This will look at your `Dangerfile` and update the pull request accordingly. While you are setting up Danger, you may want to use: `--verbose` for more debug information.
42
42
 
43
43
  ## What happens?
44
44
 
@@ -49,9 +49,9 @@ Danger runs at the end of a CI build, she will execute a `Dangerfile`. This file
49
49
    |   | Danger :no_entry_sign:
50
50
  -------------: | ------------- | ----
51
51
  :sparkles: | `lines_of_code` | The total amount of lines of code in the diff
52
- :pencil2: | `files_modified` | The list of files modified
53
- :ship: | `files_added` | The list of files added
54
- :recycle: | `files_removed` | The list of files removed
52
+ :pencil2: | `modified_files` | The list of modified files
53
+ :ship: | `added_files` | The list of added files
54
+ :recycle: | `deleted_files` | The list of removed files
55
55
  :abc: | `pr_title` | The title of the PR
56
56
  :book: | `pr_body` | The body of the PR
57
57
  :busts_in_silhouette: | `pr_author` | The author who submitted the PR
@@ -67,18 +67,18 @@ The `Dangerfile` is a ruby file, so really, you can do anything. However, at thi
67
67
  declared_trivial = pr_title.include? "#trivial"
68
68
 
69
69
  # Just to let people know
70
- warn("PR is classed as Work in Progress") if pr_title.include? "[WIP]"
70
+ warn("PR is classed as Work in Progress", sticky: false) if pr_title.include? "[WIP]"
71
71
  ```
72
72
 
73
73
  #### Being cautious around specific files
74
74
 
75
75
  ``` ruby
76
76
  # Devs shouldn't ship changes to this file
77
- fail("Developer Specific file shouldn't be changed") if files_modified.include?("Artsy/View_Controllers/App_Navigation/ARTopMenuViewController+DeveloperExtras.m")
77
+ fail("Developer Specific file shouldn't be changed", sticky: false) if modified_files.include?("Artsy/View_Controllers/App_Navigation/ARTopMenuViewController+DeveloperExtras.m")
78
78
 
79
79
  # Did you make analytics changes? Well you should also include a change to our analytics spec
80
- made_analytics_changes = files_modified.include?("/Artsy/App/ARAppDelegate+Analytics.m")
81
- made_analytics_specs_changes = files_modified.include?("/Artsy_Tests/Analytics_Tests/ARAppAnalyticsSpec.m")
80
+ made_analytics_changes = modified_files.include?("/Artsy/App/ARAppDelegate+Analytics.m")
81
+ made_analytics_specs_changes = modified_files.include?("/Artsy_Tests/Analytics_Tests/ARAppAnalyticsSpec.m")
82
82
  if made_analytics_changes
83
83
  fail("Analytics changes should have reflected specs changes") if !made_analytics_specs_changes
84
84
 
@@ -91,7 +91,7 @@ end
91
91
  #### Pinging people when a specific file has changed
92
92
 
93
93
  ```ruby
94
- message("@orta something changed in elan!") if files_modified.include? "/components/lib/variables/colors.json"
94
+ message("@orta something changed in elan!") if modified_files.include? "/components/lib/variables/colors.json"
95
95
  ```
96
96
 
97
97
  #### Exposing aspects of CI logs into the PR discussion
@@ -102,6 +102,40 @@ snapshots_url = build_log.match(%r{https://eigen-ci.s3.amazonaws.com/\d+/index.h
102
102
  fail("There were [snapshot errors](#{snapshots_url})") if snapshots_url
103
103
  ```
104
104
 
105
+ #### Available commands
106
+
107
+ Command | Description
108
+ ------------- | ----
109
+ `fail` | Causes the PR to fail and print out the error on the PR
110
+ `warn` | Prints out a warning to the PR, but still enables the merge button
111
+ `message` | Show neutral messages on the PR
112
+ `markdown` | Print raw markdown below the summary tables on the PR
113
+
114
+ ## Plugins
115
+
116
+ Danger was built with a platform in mind: It can be used with any kind of software project and allows you to write your own action to have structured source code.
117
+
118
+ In your `Dangerfile` you can import local or remote actions using
119
+
120
+ ```ruby
121
+ import "./danger_plugins/work_in_progress_warning"
122
+ # or
123
+ import "https://raw.githubusercontent.com/danger/danger/master/danger_plugins/work_in_progress_warning.rb"
124
+
125
+ # Call those actions using
126
+ work_in_progress_warning
127
+
128
+ custom_plugin(variable: "value")
129
+ ```
130
+
131
+ To create a new plugin run
132
+
133
+ ```
134
+ danger new_plugin
135
+ ```
136
+
137
+ This will generate a new Ruby file which you can modify to fit your needs.
138
+
105
139
  ## Support
106
140
 
107
141
  Danger currently is supported on Travis CI, Circle CI, BuildKite and Jenkins. These work via environment variables, so it's easy to extend to include your own.
@@ -132,6 +166,15 @@ You can tell Danger to ignore a specific warning or error by commenting on the P
132
166
  > Danger: Ignore "Developer Specific file shouldn't be changed"
133
167
  ```
134
168
 
169
+ ## Sticky
170
+
171
+ Danger can keep its history if a warning/error/message is marked as *sticky*. When the violation is resolved,
172
+ Danger will update the comment to cross it out. If you don't want this behavior, just use `sticky: false`.
173
+
174
+ ```ruby
175
+ fail("PR needs labels", sticky: false) if pr_labels.empty?
176
+ ```
177
+
135
178
  ## Useful bits of knowledge
136
179
 
137
180
  * You can set the base branch in the command line arguments see: `bundle exec danger --help`, if you commonly merge into non-master branches.
@@ -141,6 +184,8 @@ Here are some real-world Dangerfiles: [artsy/eigen](https://github.com/artsy/eig
141
184
 
142
185
  ## License, Contributor's Guidelines and Code of Conduct
143
186
 
187
+ [Join our Slack Group](https://danger-slack.herokuapp.com/)
188
+
144
189
  > This project is open source under the MIT license, which means you have full access to the source code and can modify it to fit your own needs.
145
190
 
146
191
  > This project subscribes to the [Moya Contributors Guidelines](https://github.com/Moya/contributors) which TLDR: means we give out push access easily and often.
@@ -0,0 +1,20 @@
1
+ module Danger
2
+ class Dangerfile
3
+ module DSL
4
+ class [[CLASS_NAME]] < Plugin
5
+ def run(parameter1: nil, parameter2: nil)
6
+ if (pr_body + pr_title).include?("WIP")
7
+ warn "Pull Request is Work in Progress"
8
+ end
9
+ end
10
+
11
+ def self.description
12
+ [
13
+ "Describe what this plugin does",
14
+ "and how the user can use it"
15
+ ].join(" ")
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -8,9 +8,9 @@ module Danger
8
8
  def self.scm
9
9
  [
10
10
  :lines_of_code,
11
- :files_modified,
12
- :files_deleted,
13
- :files_added,
11
+ :modified_files,
12
+ :deleted_files,
13
+ :added_files,
14
14
  :deletions,
15
15
  :insertions
16
16
  ]
@@ -1,6 +1,6 @@
1
1
  # For more info see: https://github.com/schacon/ruby-git
2
2
 
3
- require 'grit'
3
+ require 'git'
4
4
  require 'uri'
5
5
 
6
6
  module Danger
@@ -13,36 +13,38 @@ module Danger
13
13
  end
14
14
 
15
15
  def git
16
- @git ||= Grit::Git.new(".")
16
+ @git ||= GitRepo.new
17
17
  end
18
18
 
19
19
  def run_git(command)
20
- binary = Grit::Git.git_binary
21
- git.sh "#{binary} #{command}"
20
+ git.exec command
22
21
  end
23
22
 
24
- def initialize(*)
23
+ def initialize(env = {})
24
+ github_host = env["DANGER_GITHUB_HOST"] || "github.com"
25
+
25
26
  # get the remote URL
26
27
  remote = run_git "remote show origin -n | grep \"Fetch URL\" | cut -d ':' -f 2-"
27
28
  if remote
28
- remote_url_matches = remote.first.chomp.match(%r{github\.com(:|/)(?<repo_slug>.+/.+?)(?:\.git)?$})
29
+ remote_url_matches = remote.match(%r{#{Regexp.escape github_host}(:|/)(?<repo_slug>.+/.+?)(?:\.git)?$})
29
30
  if !remote_url_matches.nil? and remote_url_matches["repo_slug"]
30
31
  self.repo_slug = remote_url_matches["repo_slug"]
31
32
  else
32
- puts "Danger local requires a repository hosted on github."
33
+ puts "Danger local requires a repository hosted on GitHub.com or GitHub Enterprise."
33
34
  end
34
35
  end
35
36
 
36
37
  # get the most recent PR merge
37
- logs = run_git "log --since='2 weeks ago' --merges --oneline | grep \"Merge pull request\" | head -n 1"
38
- pr_merge = logs[0].strip
39
- if pr_merge
40
- self.pull_request_id = pr_merge.match("#[0-9]*")[0].delete("#")
41
- sha = pr_merge.split(" ")[0]
42
- parents = run_git "rev-list --parents -n 1 #{sha}"
43
- self.base_commit = parents[0].strip.split(" ")[0]
44
- self.head_commit = parents[0].strip.split(" ")[1]
38
+ pr_merge = run_git "log --since='2 weeks ago' --merges --oneline | grep \"Merge pull request\" | head -n 1".strip
39
+ if pr_merge.to_s.empty?
40
+ raise "No recent pull requests found for this repo, danger requires at least one PR for the local mode"
45
41
  end
42
+
43
+ self.pull_request_id = pr_merge.match("#([0-9]+)")[1]
44
+ sha = pr_merge.split(" ")[0]
45
+ parents = run_git("rev-list --parents -n 1 #{sha}").strip.split(" ")
46
+ self.base_commit = parents[0]
47
+ self.head_commit = parents[1]
46
48
  end
47
49
  end
48
50
  end
@@ -56,7 +56,7 @@ module Danger
56
56
  show_prompt
57
57
  answer = STDIN.gets.chomp
58
58
 
59
- break if answer.length > 0
59
+ break if answer.empty?
60
60
 
61
61
  print "\nYou need to provide an answer."
62
62
  end
@@ -22,12 +22,14 @@ module Danger
22
22
  dm.env = EnvironmentManager.new(ENV)
23
23
 
24
24
  source = dm.env.ci_source
25
- unless source.repo_slug
26
- puts "danger local".red " failed because it only works with GitHub projects at the moment. Sorry."
25
+ if source.nil? or source.repo_slug.empty?
26
+ puts "danger local failed because it only works with GitHub projects at the moment. Sorry.".red
27
27
  exit 0
28
28
  end
29
29
 
30
- puts "Running your Dangerfile against this PR - https://github.com/#{source.repo_slug}/pull/#{source.pull_request_id}"
30
+ gh = dm.env.request_source
31
+
32
+ puts "Running your Dangerfile against this PR - https://#{gh.github_host}/#{source.repo_slug}/pull/#{source.pull_request_id}"
31
33
 
32
34
  if verbose != true
33
35
  puts "Turning on --verbose"
@@ -36,7 +38,6 @@ module Danger
36
38
 
37
39
  puts ""
38
40
 
39
- gh = GitHub.new(dm.env.ci_source, ENV)
40
41
  # We can use tokenless here, as it's running on someone's computer
41
42
  # and is IP locked, as opposed to on the CI.
42
43
  gh.support_tokenless_auth = true
@@ -47,7 +48,8 @@ module Danger
47
48
  dm.env.scm = GitRepo.new
48
49
 
49
50
  dm.env.scm.diff_for_folder(".", from: dm.env.ci_source.base_commit, to: dm.env.ci_source.head_commit)
50
- dm.parse Pathname.new(@dangerfile_path)
51
+ dm.parse(Pathname.new(@dangerfile_path))
52
+ dm.print_results
51
53
  end
52
54
  end
53
55
  end
@@ -0,0 +1,43 @@
1
+ module Danger
2
+ class NewPlugin < Runner
3
+ self.summary = 'Generate a new danger plugin.'
4
+ self.command = 'new_plugin'
5
+
6
+ def initialize(argv)
7
+ super
8
+ end
9
+
10
+ def validate!
11
+ super
12
+ end
13
+
14
+ def run
15
+ require 'fileutils'
16
+
17
+ puts "Must be lower case, and use a '_' between words. Do not use '.'".green
18
+ puts "examples: 'number_of_emojis', 'ensure_pr_title_contains_keyword'".green
19
+ puts "Name of your new plugin: "
20
+ name = STDIN.gets.strip
21
+
22
+ dir = Danger.gem_path
23
+ content = File.read(File.join(dir, "lib", "assets", "PluginTemplate.rb.template"))
24
+ content.gsub!("[[CLASS_NAME]]", name.danger_class)
25
+
26
+ plugins_path = "danger_plugins"
27
+ FileUtils.mkdir_p("plugins_path") unless File.directory?(plugins_path)
28
+
29
+ output_path = File.join(plugins_path, "#{name}.rb")
30
+ raise "File '#{output_path}' already exists!" if File.exist?(output_path)
31
+ File.write(output_path, content)
32
+
33
+ puts ""
34
+ puts "Successfully created new plugin at path '#{output_path}'".green
35
+ puts "Add this to your `Dangerfile` to use it:"
36
+ puts ""
37
+ puts "import \"#{output_path.gsub('.rb', '')}\"".blue
38
+ puts "#{name}(parameter1: 123, parameter2: \"Club Mate\")".blue
39
+ puts ""
40
+ puts "Enjoy ✨"
41
+ end
42
+ end
43
+ end
@@ -2,6 +2,7 @@ module Danger
2
2
  class Runner < CLAide::Command
3
3
  require 'danger/commands/init'
4
4
  require 'danger/commands/local'
5
+ require 'danger/commands/new_plugin'
5
6
 
6
7
  self.summary = 'Run the Dangerfile.'
7
8
  self.command = 'danger'
@@ -33,22 +34,27 @@ module Danger
33
34
  dm.verbose = verbose
34
35
  dm.env = EnvironmentManager.new(ENV)
35
36
  return unless dm.env.ci_source # if it's not a PR
36
- dm.env.fill_environment_vars
37
37
 
38
- gh = dm.env.request_source
39
- ci_base = @base || gh.base_commit
40
- ci_head = @head || gh.head_commit
38
+ dm.env.fill_environment_vars
39
+ dm.env.ensure_danger_branches_are_setup
41
40
 
41
+ # Offer the chance for a user to specify a branch through the command line
42
+ ci_base = @base || dm.env.danger_head_branch
43
+ ci_head = @head || dm.env.danger_base_branch
42
44
  dm.env.scm.diff_for_folder(".", from: ci_base, to: ci_head)
43
45
 
44
46
  dm.parse Pathname.new(@dangerfile_path)
45
47
 
46
48
  post_results(dm)
49
+
50
+ dm.env.clean_up
51
+
52
+ dm.print_results
47
53
  end
48
54
 
49
55
  def post_results(dm)
50
56
  gh = dm.env.request_source
51
- gh.update_pull_request!(warnings: dm.warnings, errors: dm.errors, messages: dm.messages)
57
+ gh.update_pull_request!(warnings: dm.warnings, errors: dm.errors, messages: dm.messages, markdowns: dm.markdowns)
52
58
  end
53
59
  end
54
60
  end
@@ -1,24 +1,41 @@
1
1
  <% @tables.each do |table| %>
2
- <% if table[:content].any? %>
2
+ <% if table[:content].any? || table[:resolved].any? %>
3
3
  <table>
4
4
  <thead>
5
5
  <tr>
6
6
  <th width="50"></th>
7
- <th width="100%"><%= table[:content].count %> <%= table[:name] %><%= "s" unless table[:content].count == 1 %></th>
7
+ <th width="100%" data-kind="<%= table[:name] %>">
8
+ <% if table[:count] > 0 %>
9
+ <%= table[:count] %> <%= table[:name] %><%= "s" unless table[:count] == 1 %>
10
+ <% else %>
11
+ :white_check_mark: <%= random_compliment %>
12
+ <% end %>
13
+ </th>
8
14
  </tr>
9
15
  </thead>
10
16
  <tbody>
11
- <% table[:content].each do |message| -%>
17
+ <% table[:content].each do |violation| -%>
12
18
  <tr>
13
19
  <td>:<%= table[:emoji] %>:</td>
14
- <td><%= message %></td>
20
+ <td data-sticky="<%= violation.sticky %>"><%= violation.message %></td>
15
21
  </tr>
16
22
  <% end %>
23
+ <% table[:resolved].each do |message| -%>
24
+ <tr>
25
+ <td>:white_check_mark:</td>
26
+ <td data-sticky="true"><del><%= message %></del></td>
27
+ </tr>
28
+ <% end %>
17
29
  </tbody>
18
30
  </table>
19
31
  <% end %>
20
32
  <% end %>
21
33
 
34
+ <% @markdowns.each do |current| %>
35
+ <%= current %>
36
+ <%# the previous line has to be aligned far to the left, otherwise markdown can break easily %>
37
+ <% end %>
38
+
22
39
  <p align="right" data-meta="generated_by_danger" data-base-commit="<%= @base_commit %>" data-head-commit="<%= @head_commit %>" >
23
40
  Generated by :no_entry_sign: <a href="https://github.com/danger/danger/">danger</a>
24
41
  </p>
@@ -0,0 +1,13 @@
1
+ class String
2
+ def danger_class
3
+ split('_').collect!(&:capitalize).join
4
+ end
5
+
6
+ def danger_underscore
7
+ self.gsub(/::/, '/').
8
+ gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2').
9
+ gsub(/([a-z\d])([A-Z])/, '\1_\2').
10
+ tr("-", "_").
11
+ downcase
12
+ end
13
+ end
@@ -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,12 +15,47 @@ module Danger
13
15
  self.warnings = []
14
16
  self.errors = []
15
17
  self.messages = []
16
- load_plugins
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
17
32
  end
18
33
 
19
- def load_plugins
20
- Dir['./lib/danger/plugins/*.rb'].each do |file|
21
- require file
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 ./
22
59
  end
23
60
  end
24
61
 
@@ -30,9 +67,11 @@ module Danger
30
67
  #
31
68
  # @param [String] message
32
69
  # The message to present to the user
33
- def fail(message)
70
+ # @param [Boolean] sticky
71
+ # Whether the message should be kept after it was fixed
72
+ def fail(message, sticky: true)
34
73
  return if should_ignore_violation(message)
35
- self.errors << message
74
+ self.errors << Violation.new(message, sticky)
36
75
  puts "Raising error '#{message}'"
37
76
  end
38
77
 
@@ -40,9 +79,11 @@ module Danger
40
79
  #
41
80
  # @param [String] message
42
81
  # The message to present to the user
43
- def warn(message)
82
+ # @param [Boolean] sticky
83
+ # Whether the message should be kept after it was fixed
84
+ def warn(message, sticky: true)
44
85
  return if should_ignore_violation(message)
45
- self.warnings << message
86
+ self.warnings << Violation.new(message, sticky)
46
87
  puts "Printing warning '#{message}'"
47
88
  end
48
89
 
@@ -50,26 +91,61 @@ module Danger
50
91
  #
51
92
  # @param [String] message
52
93
  # The message to present to the user
53
- def message(message)
54
- 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)
55
98
  puts "Printing message '#{message}'"
56
99
  end
57
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
+
58
110
  # When an undefined method is called, we check to see if it's something
59
111
  # that either the `scm` or the `request_source` can handle.
60
112
  # This opens us up to letting those object extend themselves naturally.
61
- def method_missing(method_sym, *_arguments, &_block)
113
+ # This will also look for plugins
114
+ def method_missing(method_sym, *arguments, &_block)
115
+ # SCM Source
62
116
  if AvailableValues.scm.include?(method_sym)
63
- # SCM Source
64
117
  return env.scm.send(method_sym)
65
118
  end
66
119
 
120
+ # Request Source
67
121
  if AvailableValues.request_source.include?(method_sym)
68
- # Request Source
69
122
  return env.request_source.send(method_sym)
70
123
  end
71
124
 
72
- raise "Unknown method '#{method_sym}', please check out the documentation for available variables".red
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
73
149
  end
74
150
  end
75
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
@@ -1,21 +1,32 @@
1
1
  module Danger
2
2
  class Dangerfile
3
3
  module DSL
4
- # Protect a file from being changed. This can
5
- # be used in combination with some kind of
6
- # permission check if a user is inside the org
7
- def protect_files(path: nil, message: nil, fail_build: true)
8
- broken_rule = false
9
- Dir.glob(path) do |current|
10
- broken_rule = true if self.env.scm.files_modified.include?(current)
11
- end
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
12
14
 
13
- return unless broken_rule
15
+ return unless broken_rule
16
+
17
+ if fail_build
18
+ @dsl.errors << message
19
+ else
20
+ @dsl.warnings << message
21
+ end
22
+ end
14
23
 
15
- if fail_build
16
- self.errors << message
17
- else
18
- self.messages << message
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(" ")
19
30
  end
20
31
  end
21
32
  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, :ignored_violations
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,19 +12,23 @@ 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
@@ -52,6 +56,10 @@ module Danger
52
56
  self.pr_json[:head][:sha]
53
57
  end
54
58
 
59
+ def branch_for_merge
60
+ self.pr_json[:base][:ref]
61
+ end
62
+
55
63
  def pr_title
56
64
  self.pr_json[:title].to_s
57
65
  end
@@ -69,16 +77,28 @@ module Danger
69
77
  end
70
78
 
71
79
  # Sending data to GitHub
72
- def update_pull_request!(warnings: nil, errors: nil, messages: nil)
80
+ def update_pull_request!(warnings: [], errors: [], messages: [], markdowns: [])
73
81
  comment_result = {}
74
82
 
75
- 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?
76
94
  # Just remove the comment, if there's nothing to say.
77
95
  delete_old_comments!
78
96
  else
79
- issues = client.issue_comments(ci_source.repo_slug, ci_source.pull_request_id)
80
- editable_issues = issues.reject { |issue| issue[:body].include?("generated_by_danger") == false }
81
- 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)
82
102
 
83
103
  if editable_issues.empty?
84
104
  comment_result = client.add_comment(ci_source.repo_slug, ci_source.pull_request_id, body)
@@ -100,7 +120,7 @@ module Danger
100
120
  message = generate_github_description(warnings: warnings, errors: errors)
101
121
  client.create_status(ci_source.repo_slug, latest_pr_commit_ref, status, {
102
122
  description: message,
103
- context: "KrauseFx/danger",
123
+ context: "danger/danger",
104
124
  target_url: details_url
105
125
  })
106
126
  rescue
@@ -125,11 +145,15 @@ module Danger
125
145
  end
126
146
  end
127
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
+
128
154
  def generate_github_description(warnings: nil, errors: nil)
129
155
  if errors.empty? && warnings.empty?
130
- compliment = ["Well done.", "Congrats.", "Woo!",
131
- "Yay.", "Jolly good show.", "Good on 'ya.", "Nice work."]
132
- return "All green. #{compliment.sample}"
156
+ return "All green. #{random_compliment}"
133
157
  else
134
158
  message = "⚠ "
135
159
  message += "#{errors.count} Error#{errors.count == 1 ? '' : 's'}. " unless errors.empty?
@@ -139,7 +163,7 @@ module Danger
139
163
  end
140
164
  end
141
165
 
142
- def generate_comment(warnings: [], errors: [], messages: [])
166
+ def generate_comment(warnings: [], errors: [], messages: [], markdowns: [], previous_violations: {})
143
167
  require 'erb'
144
168
 
145
169
  md_template = File.join(Danger.gem_path, "lib/danger/comment_generators/github.md.erb")
@@ -147,18 +171,64 @@ module Danger
147
171
  # erb: http://www.rrn.dk/rubys-erb-templating-system
148
172
  # for the extra args: http://stackoverflow.com/questions/4632879/erb-template-removing-the-trailing-line
149
173
  @tables = [
150
- { name: "Error", emoji: "no_entry_sign", content: errors.map { |s| process_markdown(s) } },
151
- { name: "Warning", emoji: "warning", content: warnings.map { |s| process_markdown(s) } },
152
- { 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)
153
177
  ]
178
+ @markdowns = markdowns
179
+
154
180
  return ERB.new(File.read(md_template), 0, "-").result(binding)
155
181
  end
156
182
 
157
- def process_markdown(string)
158
- 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)
159
229
  match = html.match(%r{^<p>(.*)</p>$})
160
- return match.captures.first unless match.nil?
161
- html
230
+ message = match.nil? ? html : match.captures.first
231
+ Violation.new(message, violation.sticky)
162
232
  end
163
233
  end
164
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.6.0"
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"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: danger
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Orta Therox
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2016-03-10 00:00:00.000000000 Z
12
+ date: 2016-04-07 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: claide
@@ -26,19 +26,19 @@ dependencies:
26
26
  - !ruby/object:Gem::Version
27
27
  version: '0'
28
28
  - !ruby/object:Gem::Dependency
29
- name: grit
29
+ name: git
30
30
  requirement: !ruby/object:Gem::Requirement
31
31
  requirements:
32
- - - '='
32
+ - - ">="
33
33
  - !ruby/object:Gem::Version
34
- version: 2.5.0
34
+ version: '0'
35
35
  type: :runtime
36
36
  prerelease: false
37
37
  version_requirements: !ruby/object:Gem::Requirement
38
38
  requirements:
39
- - - '='
39
+ - - ">="
40
40
  - !ruby/object:Gem::Version
41
- version: 2.5.0
41
+ version: '0'
42
42
  - !ruby/object:Gem::Dependency
43
43
  name: colored
44
44
  requirement: !ruby/object:Gem::Requirement
@@ -185,14 +185,14 @@ dependencies:
185
185
  requirements:
186
186
  - - "~>"
187
187
  - !ruby/object:Gem::Version
188
- version: 0.35.1
188
+ version: 0.38.0
189
189
  type: :development
190
190
  prerelease: false
191
191
  version_requirements: !ruby/object:Gem::Requirement
192
192
  requirements:
193
193
  - - "~>"
194
194
  - !ruby/object:Gem::Version
195
- version: 0.35.1
195
+ version: 0.38.0
196
196
  description: Create a Dangerfile to introspect your pull request in CI, makes it easy
197
197
  to enforce social conventions like changelogs and tests.
198
198
  email:
@@ -207,6 +207,7 @@ files:
207
207
  - README.md
208
208
  - bin/danger
209
209
  - lib/assets/DangerfileTemplate
210
+ - lib/assets/PluginTemplate.rb.template
210
211
  - lib/danger.rb
211
212
  - lib/danger/available_values.rb
212
213
  - lib/danger/ci_source/buildkite.rb
@@ -219,16 +220,21 @@ files:
219
220
  - lib/danger/commands/init.rb
220
221
  - lib/danger/commands/init_helpers/interviewer.rb
221
222
  - lib/danger/commands/local.rb
223
+ - lib/danger/commands/new_plugin.rb
222
224
  - lib/danger/commands/runner.rb
223
225
  - lib/danger/comment_generators/github.md.erb
226
+ - lib/danger/core_ext/string.rb
224
227
  - lib/danger/dangerfile.rb
225
228
  - lib/danger/dangerfile_dsl.rb
226
229
  - lib/danger/environment_manager.rb
230
+ - lib/danger/plugins/plugin.rb
227
231
  - lib/danger/plugins/protect_files.rb
228
232
  - lib/danger/request_sources/github.rb
233
+ - lib/danger/scm_source/file_list.rb
229
234
  - lib/danger/scm_source/git_repo.rb
230
235
  - lib/danger/standard_error.rb
231
236
  - lib/danger/version.rb
237
+ - lib/danger/violation.rb
232
238
  homepage: http://github.com/danger/danger
233
239
  licenses:
234
240
  - MIT
@@ -249,9 +255,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
249
255
  version: '0'
250
256
  requirements: []
251
257
  rubyforge_project:
252
- rubygems_version: 2.2.2
258
+ rubygems_version: 2.4.6
253
259
  signing_key:
254
260
  specification_version: 4
255
261
  summary: Automate your PR etiquette.
256
262
  test_files: []
257
- has_rdoc: