danger 0.8.1 → 0.8.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1063da82f82263eede8fc418f7d85a7f1e4a2c4c
4
- data.tar.gz: f7bb1935826bf866378aa91c79cb85255ebe25fb
3
+ metadata.gz: 961961730193f38ada39854b1e3b14a665534539
4
+ data.tar.gz: 161dfd213ef838bf04dc47c95cd3a59d5e745cc7
5
5
  SHA512:
6
- metadata.gz: 15724839453717cca2a7ff70da0bc9cbf6940605defde3d713205690d54a4a2900aaf8dd3843e5a6a7e4cb4a4b2d9c3f7efed806490ad70bfed218a7fe126f76
7
- data.tar.gz: 7c55ff88479f5aba1be83f0e0a70492f30186170b15bc3815e74f56f3807599219a88efadd6f92106987d4e7aeb3d72585887e6587854ba60e3d4bdc19a66bef
6
+ metadata.gz: 9c298a712a18d76022aba76603f6946f03ef5999a2203c06c4cc34013e2131d9657fa3ad2972b0bbf9c406f9d6922f5d2677160068482aaf0fbd3572444a31df
7
+ data.tar.gz: b657e664d71d270a6118bfe68033f039ac2604f969d9494b2e16631e4a9b65448b7e8de1842909e628da7a97aae65ba72939b7b9dc3e2783d8bd33ec62946056
data/README.md CHANGED
@@ -43,7 +43,7 @@ This will look at your `Dangerfile` and update the pull request accordingly. Whi
43
43
 
44
44
  ## CI Support
45
45
 
46
- Danger currently is supported on Travis CI, Circle CI, Xcode Bots via Buildasaur, BuildKite, Jenkins, Semaphore, and Drone CI. These work via environment variables, so it's easy to extend to include your own.
46
+ Danger currently is supported on Travis CI, Circle CI, Xcode Bots via Buildasaur, BuildKite, Jenkins, Semaphore, Drone CI and TeamCity. These work via environment variables, so it's easy to extend to include your own.
47
47
 
48
48
  ## What happens?
49
49
 
@@ -62,7 +62,7 @@ Danger runs at the end of a CI build, she will execute a `Dangerfile`. This file
62
62
  :busts_in_silhouette: | `pr_author` | The author who submitted the PR
63
63
  :bookmark: | `pr_labels` | The labels added to the PR
64
64
 
65
- The `Dangerfile` is a ruby file, so really, you can do anything. However, at this stage you might need selling on the idea a bit more, so lets take some real examples:
65
+ The `Dangerfile` is a Ruby file, so really, you can do anything. However, at this stage you might need selling on the idea a bit more, so lets take some real examples:
66
66
 
67
67
  #### Dealing with WIP pull requests
68
68
 
@@ -192,6 +192,20 @@ Danger will update the comment to cross it out. If you don't want this behavior,
192
192
  fail("PR needs labels", sticky: false) if pr_labels.empty?
193
193
  ```
194
194
 
195
+ ## Multiple Dangers
196
+
197
+ If one Danger is not enough for you, you can run several ones on the same PR. Just use the `danger_id` param. For example:
198
+
199
+ ```
200
+ bundle exec danger --danger_id=unit_tests
201
+ ```
202
+
203
+ You can have each instance of Danger running on a different CI provider and even doing different validations. An use case would be:
204
+
205
+ * `basic` runs on a Linux environment (such as Circle CI) and validates the PR itself (title, etc)
206
+ * `compilation` runs on a Mac after running unit tests for your iOS app and comments about warnings, test failures, etc
207
+ * `uitests` runs on a Mac after running UI Unit tests and comments about test failures
208
+
195
209
  ## Useful bits of knowledge
196
210
 
197
211
  * You can set the base branch in the command line arguments see: `bundle exec danger --help`, if you commonly merge into non-master branches.
@@ -206,7 +220,7 @@ Danger allows usage with GitHub Enterprise by setting 2 environment variables:
206
220
 
207
221
  ## License, Contributor's Guidelines and Code of Conduct
208
222
 
209
- [Join our Slack Group](https://danger-slack.herokuapp.com/)
223
+ We try to keep as much discussion as possible in GitHub issues, but also have a slack, if you'd like an invite ping [@Orta](https://twitter.com/orta/) a DM on twitter with your email.
210
224
 
211
225
  > 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.
212
226
 
@@ -5,17 +5,23 @@ module Danger
5
5
  module CISource
6
6
  class Buildkite < CI
7
7
  def self.validates?(env)
8
- return !env["BUILDKITE"].nil?
8
+ return false unless env["BUILDKITE"]
9
+ return false unless env["BUILDKITE_REPO"]
10
+ return false unless env["BUILDKITE_PULL_REQUEST"]
11
+
12
+ return true
9
13
  end
10
14
 
11
15
  def initialize(env)
12
- repo = env["BUILDKITE_REPO"]
13
- unless repo.nil?
14
- repo_matches = repo.match(%r{([\/:])([^\/]+\/[^\/.]+)(?:.git)?$})
15
- self.repo_slug = repo_matches[2] unless repo_matches.nil?
16
- end
17
-
16
+ self.repo_url = env["BUILDKITE_REPO"]
18
17
  self.pull_request_id = env["BUILDKITE_PULL_REQUEST"]
18
+
19
+ repo_matches = self.repo_url.match(%r{([\/:])([^\/]+\/[^\/.]+)(?:.git)?$})
20
+ self.repo_slug = repo_matches[2] unless repo_matches.nil?
21
+ end
22
+
23
+ def supported_request_sources
24
+ @supported_request_sources ||= [Danger::RequestSources::GitHub]
19
25
  end
20
26
  end
21
27
  end
@@ -2,7 +2,24 @@ module Danger
2
2
  module CISource
3
3
  # "abstract" CI class
4
4
  class CI
5
- attr_accessor :repo_slug, :pull_request_id
5
+ attr_accessor :repo_slug, :pull_request_id, :repo_url, :supported_request_sources
6
+
7
+ def self.inherited(child_class)
8
+ available_ci_sources.add child_class
9
+ super
10
+ end
11
+
12
+ def self.available_ci_sources
13
+ @available_ci_sources ||= Set.new
14
+ end
15
+
16
+ def supported_request_sources
17
+ raise "CISource subclass must specify the supported request sources"
18
+ end
19
+
20
+ def supports?(request_source)
21
+ supported_request_sources.include? request_source
22
+ end
6
23
 
7
24
  def self.validates?(_env)
8
25
  false
@@ -6,10 +6,16 @@ module Danger
6
6
  module CISource
7
7
  class CircleCI < CI
8
8
  def self.validates?(env)
9
- return false if env["CIRCLE_BUILD_NUM"].nil?
10
- return true unless env["CI_PULL_REQUEST"].nil?
9
+ return false unless env["CIRCLE_BUILD_NUM"]
10
+ return false unless env["CI_PULL_REQUEST"]
11
+ return false unless env["CIRCLE_PROJECT_USERNAME"]
12
+ return false unless env["CIRCLE_PROJECT_REPONAME"]
11
13
 
12
- return !env["CIRCLE_PROJECT_USERNAME"].nil? && !env["CIRCLE_PROJECT_REPONAME"].nil?
14
+ return true
15
+ end
16
+
17
+ def supported_request_sources
18
+ @supported_request_sources ||= [Danger::RequestSources::GitHub]
13
19
  end
14
20
 
15
21
  def client
@@ -33,6 +39,8 @@ module Danger
33
39
  end
34
40
 
35
41
  def initialize(env)
42
+ self.repo_url = GitRepo.new.origins # CircleCI doesn't provide a repo url env variable :/
43
+
36
44
  @circle_token = env["CIRCLE_CI_API_TOKEN"]
37
45
  url = pull_request_url(env)
38
46
 
@@ -4,14 +4,21 @@ module Danger
4
4
  module CISource
5
5
  class Drone < CI
6
6
  def self.validates?(env)
7
- return !env["DRONE"].nil?
7
+ return false unless env["DRONE"]
8
+ return false unless env["DRONE_REPO"]
9
+ return false unless env["DRONE_PULL_REQUEST"].to_i > 0
10
+
11
+ return true
12
+ end
13
+
14
+ def supported_request_sources
15
+ @supported_request_sources ||= [Danger::RequestSources::GitHub]
8
16
  end
9
17
 
10
18
  def initialize(env)
11
19
  self.repo_slug = env["DRONE_REPO"]
12
- if env["DRONE_PULL_REQUEST"].to_i > 0
13
- self.pull_request_id = env["DRONE_PULL_REQUEST"]
14
- end
20
+ self.pull_request_id = env["DRONE_PULL_REQUEST"]
21
+ self.repo_url = GitRepo.new.origins # Drone doesn't provide a repo url env variable :/
15
22
  end
16
23
  end
17
24
  end
@@ -5,20 +5,22 @@ module Danger
5
5
  module CISource
6
6
  class Jenkins < CI
7
7
  def self.validates?(env)
8
- return !env["ghprbPullId"].nil? && !env["GIT_URL"].nil?
8
+ return false unless env["ghprbPullId"].to_i > 0
9
+ return false unless env["GIT_URL"]
10
+
11
+ return true
12
+ end
13
+
14
+ def supported_request_sources
15
+ @supported_request_sources ||= [Danger::RequestSources::GitHub]
9
16
  end
10
17
 
11
18
  def initialize(env)
12
- repo = env["GIT_URL"]
13
- unless repo.nil?
14
- repo_matches = repo.match(%r{([\/:])([^\/]+\/[^\/.]+)(?:.git)?$})
15
- self.repo_slug = repo_matches[2] unless repo_matches.nil?
16
- end
19
+ self.repo_url = env["GIT_URL"]
20
+ self.pull_request_id = env["ghprbPullId"]
17
21
 
18
- # from https://docs.travis-ci.com/user/pull-requests, as otherwise it's "false"
19
- if env["ghprbPullId"].to_i > 0
20
- self.pull_request_id = env["ghprbPullId"]
21
- end
22
+ repo_matches = self.repo_url.match(%r{([\/:])([^\/]+\/[^\/.]+)(?:.git)?$})
23
+ self.repo_slug = repo_matches[2] unless repo_matches.nil?
22
24
  end
23
25
  end
24
26
  end
@@ -20,6 +20,10 @@ module Danger
20
20
  git.exec command
21
21
  end
22
22
 
23
+ def supported_request_sources
24
+ @supported_request_sources ||= [Danger::RequestSources::GitHub]
25
+ end
26
+
23
27
  def initialize(env = {})
24
28
  github_host = env["DANGER_GITHUB_HOST"] || "github.com"
25
29
 
@@ -4,14 +4,21 @@ module Danger
4
4
  module CISource
5
5
  class Semaphore < CI
6
6
  def self.validates?(env)
7
- return !env["SEMAPHORE"].nil?
7
+ return false unless env["SEMAPHORE"]
8
+ return false unless env["SEMAPHORE_REPO_SLUG"]
9
+ return false unless env["PULL_REQUEST_NUMBER"].to_i > 0
10
+
11
+ return true
12
+ end
13
+
14
+ def supported_request_sources
15
+ @supported_request_sources ||= [Danger::RequestSources::GitHub]
8
16
  end
9
17
 
10
18
  def initialize(env)
11
19
  self.repo_slug = env["SEMAPHORE_REPO_SLUG"]
12
- if env["PULL_REQUEST_NUMBER"].to_i > 0
13
- self.pull_request_id = env["PULL_REQUEST_NUMBER"]
14
- end
20
+ self.pull_request_id = env["PULL_REQUEST_NUMBER"]
21
+ self.repo_url = GitRepo.new.origins # Semaphore doesn't provide a repo url env variable :/
15
22
  end
16
23
  end
17
24
  end
@@ -0,0 +1,22 @@
1
+ module Danger
2
+ module CISource
3
+ class TeamCity < CI
4
+ def self.validates?(env)
5
+ env.key? 'TEAMCITY_VERSION'
6
+ end
7
+
8
+ def supported_request_sources
9
+ @supported_request_sources ||= [Danger::RequestSources::GitHub]
10
+ end
11
+
12
+ def initialize(env)
13
+ # NB: Unfortunately TeamCity doesn't provide these variables
14
+ # automatically so you have to add these variables manually to your
15
+ # project or build configuration
16
+ self.repo_slug = env['GITHUB_REPO_SLUG']
17
+ self.pull_request_id = env['GITHUB_PULL_REQUEST_ID'].to_i
18
+ self.repo_url = env['GITHUB_REPO_URL']
19
+ end
20
+ end
21
+ end
22
+ end
@@ -5,15 +5,23 @@ module Danger
5
5
  module CISource
6
6
  class Travis < CI
7
7
  def self.validates?(env)
8
- return !env["HAS_JOSH_K_SEAL_OF_APPROVAL"].nil?
8
+ return false unless env["HAS_JOSH_K_SEAL_OF_APPROVAL"]
9
+ return false unless env["TRAVIS_REPO_SLUG"]
10
+ return false unless env["TRAVIS_PULL_REQUEST"]
11
+
12
+ return true
13
+ end
14
+
15
+ def supported_request_sources
16
+ @supported_request_sources ||= [Danger::RequestSources::GitHub]
9
17
  end
10
18
 
11
19
  def initialize(env)
12
20
  self.repo_slug = env["TRAVIS_REPO_SLUG"]
13
- # from https://docs.travis-ci.com/user/pull-requests, as otherwise it's "false"
14
21
  if env["TRAVIS_PULL_REQUEST"].to_i > 0
15
22
  self.pull_request_id = env["TRAVIS_PULL_REQUEST"]
16
23
  end
24
+ self.repo_url = GitRepo.new.origins # Travis doesn't provide a repo url env variable :/
17
25
  end
18
26
  end
19
27
  end
@@ -4,7 +4,13 @@ module Danger
4
4
  module CISource
5
5
  class XcodeServer < CI
6
6
  def self.validates?(env)
7
- return !env["XCS_BOT_NAME"].nil?
7
+ return false unless env["XCS_BOT_NAME"]
8
+
9
+ return true
10
+ end
11
+
12
+ def supported_request_sources
13
+ @supported_request_sources ||= [Danger::RequestSources::GitHub]
8
14
  end
9
15
 
10
16
  def initialize(env)
@@ -15,6 +21,7 @@ module Danger
15
21
  self.repo_slug = repo_matches[1] unless repo_matches.nil?
16
22
  pull_request_id_matches = bot_name.match(/#(\d+)/)
17
23
  self.pull_request_id = pull_request_id_matches[1] unless pull_request_id_matches.nil?
24
+ self.repo_url = GitRepo.new.origins # Xcode Server doesn't provide a repo url env variable :/
18
25
  end
19
26
  end
20
27
  end
@@ -15,6 +15,7 @@ module Danger
15
15
  @dangerfile_path = dangerfile if File.exist? dangerfile
16
16
  @base = argv.option('base')
17
17
  @head = argv.option('head')
18
+ @danger_id = argv.option('danger_id', 'danger')
18
19
  super
19
20
  end
20
21
 
@@ -29,7 +30,8 @@ module Danger
29
30
  [
30
31
  ['--base=[master|dev|stable]', 'A branch/tag/commit to use as the base of the diff'],
31
32
  ['--head=[master|dev|stable]', 'A branch/tag/commit to use as the head'],
32
- ['--dangerfile=<path/to/dangerfile>', 'The location of your Dangerfile']
33
+ ['--dangerfile=<path/to/dangerfile>', 'The location of your Dangerfile'],
34
+ ['--danger_id=<id>', 'The identifier of this Danger instance']
33
35
  ].concat(super)
34
36
  end
35
37
 
@@ -47,8 +49,8 @@ module Danger
47
49
  dm.env.ensure_danger_branches_are_setup
48
50
 
49
51
  # Offer the chance for a user to specify a branch through the command line
50
- ci_base = @base || dm.env.danger_head_branch
51
- ci_head = @head || dm.env.danger_base_branch
52
+ ci_base = @base || EnvironmentManager.danger_base_branch
53
+ ci_head = @head || EnvironmentManager.danger_head_branch
52
54
  dm.env.scm.diff_for_folder(".", from: ci_base, to: ci_head)
53
55
 
54
56
  dm.parse Pathname.new(@dangerfile_path)
@@ -68,7 +70,19 @@ module Danger
68
70
  violations = dm.violation_report
69
71
  status = dm.status_report
70
72
 
71
- gh.update_pull_request!(warnings: violations[:warnings], errors: violations[:errors], messages: violations[:messages], markdowns: status[:markdowns])
73
+ gh.update_pull_request!(warnings: violations[:warnings], errors: violations[:errors], messages: violations[:messages], markdowns: status[:markdowns], danger_id: @danger_id)
74
+ end
75
+
76
+ def self.report_error(exception)
77
+ raise exception if exception.kind_of?(SystemExit)
78
+ message = "#{exception.message.red} (#{exception.class.to_s.yellow})"
79
+ if exception.backtrace
80
+ danger_lib = File.expand_path('../../..', __FILE__)
81
+ message << "\n\t" << exception.backtrace.reverse_each.
82
+ drop_while { |bt| !bt.start_with?(danger_lib) }.reverse.
83
+ join("\n\t")
84
+ end
85
+ abort(message)
72
86
  end
73
87
  end
74
88
  end
@@ -1,41 +1,40 @@
1
- <% @tables.each do |table| %>
2
- <% if table[:content].any? || table[:resolved].any? %>
1
+ <%- @tables.each do |table| -%>
2
+ <%- if table[:content].any? || table[:resolved].any? -%>
3
3
  <table>
4
4
  <thead>
5
5
  <tr>
6
6
  <th width="50"></th>
7
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 %>
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
13
  </th>
14
14
  </tr>
15
15
  </thead>
16
16
  <tbody>
17
- <% table[:content].each do |violation| -%>
17
+ <%- table[:content].each do |violation| -%>
18
18
  <tr>
19
19
  <td>:<%= table[:emoji] %>:</td>
20
20
  <td data-sticky="<%= violation.sticky %>"><%= violation.message %></td>
21
21
  </tr>
22
- <% end %>
23
- <% table[:resolved].each do |message| -%>
22
+ <%- end -%>
23
+ <%- table[:resolved].each do |message| -%>
24
24
  <tr>
25
25
  <td>:white_check_mark:</td>
26
26
  <td data-sticky="true"><del><%= message %></del></td>
27
27
  </tr>
28
- <% end %>
28
+ <%- end -%>
29
29
  </tbody>
30
30
  </table>
31
- <% end %>
32
- <% end %>
31
+ <%- end -%>
32
+ <%- end -%>
33
33
 
34
- <% @markdowns.each do |current| %>
34
+ <%- @markdowns.each do |current| -%>
35
35
  <%= current %>
36
36
  <%# the previous line has to be aligned far to the left, otherwise markdown can break easily %>
37
- <% end %>
38
-
39
- <p align="right" data-meta="generated_by_danger">
37
+ <%- end -%>
38
+ <p align="right" data-meta="generated_by_<%= @danger_id %>">
40
39
  Generated by :no_entry_sign: <a href="https://github.com/danger/danger/">danger</a>
41
40
  </p>
@@ -82,7 +82,7 @@ module Danger
82
82
  # Iterate through available plugin classes and initialize them with
83
83
  # a reference to this Dangerfile
84
84
  def refresh_plugins
85
- plugins = ObjectSpace.each_object(Class).select { |klass| klass < Danger::Plugin }
85
+ plugins = Plugin.all_plugins
86
86
  plugins.each do |klass|
87
87
  next if klass.respond_to?(:singleton_class?) && klass.singleton_class?
88
88
  plugin = klass.new(self)
@@ -1,17 +1,15 @@
1
1
  require "danger/ci_source/ci_source"
2
- require "danger/request_source/github"
2
+ require "danger/request_source/request_source"
3
3
 
4
4
  module Danger
5
5
  class EnvironmentManager
6
6
  attr_accessor :ci_source, :request_source, :scm
7
7
 
8
8
  def initialize(env)
9
- CISource.constants.each do |symb|
10
- c = CISource.const_get(symb)
11
- next unless c.kind_of?(Class)
12
- next unless c.validates?(env)
9
+ CISource::CI.available_ci_sources.each do |klass|
10
+ next unless klass.validates?(env)
13
11
 
14
- self.ci_source = c.new(env)
12
+ self.ci_source = klass.new(env)
15
13
  if self.ci_source.repo_slug and self.ci_source.pull_request_id
16
14
  break
17
15
  else
@@ -20,12 +18,19 @@ module Danger
20
18
  end
21
19
  end
22
20
 
23
- raise "Could not find a CI source".red unless self.ci_source
21
+ raise "Could not find a valid pull request within the known CI sources".red unless self.ci_source
24
22
 
25
- # only GitHub for now, open for PRs adding more!
26
- self.request_source = GitHub.new(self.ci_source, ENV)
27
- # Also Git only for now, also open for PRs adding more!
28
- self.scm = GitRepo.new # For now
23
+ RequestSources::RequestSource.available_request_sources.each do |klass|
24
+ next unless self.ci_source.supports?(klass)
25
+
26
+ request_source = klass.new(self.ci_source, ENV)
27
+ next unless request_source.validates?
28
+ self.request_source = request_source
29
+ end
30
+
31
+ raise "Could not find a Request Source".red unless self.request_source
32
+
33
+ self.scm = self.request_source.scm
29
34
  end
30
35
 
31
36
  def pr?
@@ -39,40 +44,28 @@ module Danger
39
44
  def ensure_danger_branches_are_setup
40
45
  clean_up
41
46
 
42
- # As this currently just works with GitHub, we can use a github specific feature here:
43
- pull_id = ci_source.pull_request_id
44
-
45
- # TODO: This isn't optimal, should be hidden behind some kind of facade, but the plugin makes that
46
- # difficult to do without accessing the dangerfile
47
- test_branch = request_source.pr_json[:base][:sha]
48
-
49
- # Next, we want to ensure that we have a version of the current branch at a known location
50
- scm.exec "branch #{danger_base_branch} #{test_branch}"
51
-
52
- # OK, so we want to ensure that we have a known head branch, this will always represent
53
- # the head of the PR ( e.g. the most recent commit that will be merged. )
54
- scm.exec "fetch origin +refs/pull/#{pull_id}/merge:#{danger_head_branch}"
47
+ self.request_source.setup_danger_branches
55
48
  end
56
49
 
57
50
  def clean_up
58
- [danger_base_branch, danger_base_branch].each do |branch|
51
+ [EnvironmentManager.danger_base_branch, EnvironmentManager.danger_head_branch].each do |branch|
59
52
  scm.exec("branch -D #{branch}") unless scm.exec("rev-parse --quiet --verify #{branch}").empty?
60
53
  end
61
54
  end
62
55
 
63
56
  def meta_info_for_base
64
- scm.exec("--no-pager log #{danger_base_branch} -n1")
57
+ scm.exec("--no-pager log #{EnvironmentManager.danger_base_branch} -n1")
65
58
  end
66
59
 
67
60
  def meta_info_for_head
68
- scm.exec("--no-pager log #{danger_head_branch} -n1")
61
+ scm.exec("--no-pager log #{EnvironmentManager.danger_head_branch} -n1")
69
62
  end
70
63
 
71
- def danger_head_branch
64
+ def self.danger_head_branch
72
65
  "danger_head"
73
66
  end
74
67
 
75
- def danger_base_branch
68
+ def self.danger_base_branch
76
69
  "danger_base"
77
70
  end
78
71
  end
@@ -4,7 +4,7 @@ module Danger
4
4
  class DangerfileGitHubPlugin < Plugin
5
5
  def initialize(dangerfile)
6
6
  super(dangerfile)
7
- return nil unless dangerfile.env.request_source.class == Danger::GitHub
7
+ return nil unless dangerfile.env.request_source.class == Danger::RequestSources::GitHub
8
8
 
9
9
  @github = dangerfile.env.request_source
10
10
  end
@@ -5,7 +5,7 @@ module Danger
5
5
  end
6
6
 
7
7
  def self.instance_name
8
- self.to_s.gsub("Danger", "").danger_underscore.split('/').last
8
+ to_s.gsub("Danger", "").danger_underscore.split('/').last
9
9
  end
10
10
 
11
11
  # Both of these methods exist on all objects
@@ -13,19 +13,20 @@ module Danger
13
13
  # http://ruby-doc.org/core-2.2.3/Kernel.html#method-i-fail
14
14
  # However, as we're using using them in the DSL, they won't
15
15
  # get method_missing called correctly.
16
+ undef :warn, :fail
16
17
 
17
- def warn(*args, &blk)
18
- method_missing(:warn, *args, &blk)
18
+ # Since we have a reference to the Dangerfile containing all the information
19
+ # We need to redirect the self calls to the Dangerfile
20
+ def method_missing(method_sym, *arguments, &block)
21
+ @dangerfile.send(method_sym, *arguments, &block)
19
22
  end
20
23
 
21
- def fail(*args, &blk)
22
- method_missing(:fail, *args, &blk)
24
+ def self.all_plugins
25
+ @all_plugins ||= []
23
26
  end
24
27
 
25
- # Since we have a reference to the Dangerfile containing all the information
26
- # We need to redirect the self calls to the Dangerfile
27
- def method_missing(method_sym, *arguments, &_block)
28
- @dangerfile.send(method_sym, *arguments)
28
+ def self.inherited(plugin)
29
+ Plugin.all_plugins.push(plugin)
29
30
  end
30
31
  end
31
32
  end
@@ -3,201 +3,225 @@ require 'octokit'
3
3
  require 'redcarpet'
4
4
 
5
5
  module Danger
6
- class GitHub
7
- attr_accessor :ci_source, :pr_json, :issue_json, :environment, :support_tokenless_auth, :ignored_violations, :github_host
6
+ module RequestSources
7
+ class GitHub < RequestSource
8
+ attr_accessor :pr_json, :issue_json, :support_tokenless_auth
9
+
10
+ def initialize(ci_source, environment)
11
+ self.ci_source = ci_source
12
+ self.environment = environment
13
+ self.support_tokenless_auth = false
14
+
15
+ Octokit.auto_paginate = true
16
+ @token = @environment["DANGER_GITHUB_API_TOKEN"]
17
+ if @environment["DANGER_GITHUB_API_HOST"]
18
+ Octokit.api_endpoint = @environment["DANGER_GITHUB_API_HOST"]
19
+ end
20
+ end
8
21
 
9
- def initialize(ci_source, environment)
10
- self.ci_source = ci_source
11
- self.environment = environment
12
- self.support_tokenless_auth = false
22
+ def scm
23
+ @scm ||= GitRepo.new
24
+ end
13
25
 
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"]
26
+ def host
27
+ @host = @environment["DANGER_GITHUB_HOST"] || "github.com"
19
28
  end
20
- end
21
29
 
22
- def client
23
- raise "No API token given, please provide one using `DANGER_GITHUB_API_TOKEN`" if !@token && !support_tokenless_auth
24
- @client ||= Octokit::Client.new(access_token: @token)
25
- end
30
+ def client
31
+ raise "No API token given, please provide one using `DANGER_GITHUB_API_TOKEN`" if !@token && !support_tokenless_auth
32
+ @client ||= Octokit::Client.new(access_token: @token)
33
+ end
26
34
 
27
- def markdown_parser
28
- @markdown_parser ||= Redcarpet::Markdown.new(Redcarpet::Render::HTML, no_intra_emphasis: true)
29
- end
35
+ def markdown_parser
36
+ @markdown_parser ||= Redcarpet::Markdown.new(Redcarpet::Render::HTML, no_intra_emphasis: true)
37
+ end
30
38
 
31
- def fetch_details
32
- self.pr_json = client.pull_request(ci_source.repo_slug, ci_source.pull_request_id)
33
- fetch_issue_details(self.pr_json)
34
- self.ignored_violations = ignored_violations_from_pr(self.pr_json)
35
- end
39
+ def setup_danger_branches
40
+ # we can use a github specific feature here:
41
+ pull_id = self.ci_source.pull_request_id
42
+ test_branch = self.pr_json[:base][:sha]
36
43
 
37
- def ignored_violations_from_pr(pr_json)
38
- pr_body = pr_json[:body]
39
- return [] if pr_body.nil?
40
- pr_body.chomp.scan(/>\s*danger\s*:\s*ignore\s*"(.*)"/i).flatten
41
- end
44
+ # Next, we want to ensure that we have a version of the current branch at a known location
45
+ self.scm.exec "branch #{EnvironmentManager.danger_base_branch} #{test_branch}"
42
46
 
43
- def fetch_issue_details(pr_json)
44
- href = pr_json[:_links][:issue][:href]
45
- self.issue_json = client.get(href)
46
- end
47
+ # OK, so we want to ensure that we have a known head branch, this will always represent
48
+ # the head of the PR ( e.g. the most recent commit that will be merged. )
49
+ self.scm.exec "fetch origin +refs/pull/#{pull_id}/merge:#{EnvironmentManager.danger_head_branch}"
50
+ end
47
51
 
48
- # Sending data to GitHub
49
- def update_pull_request!(warnings: [], errors: [], messages: [], markdowns: [])
50
- comment_result = {}
52
+ def fetch_details
53
+ self.pr_json = client.pull_request(ci_source.repo_slug, ci_source.pull_request_id)
54
+ fetch_issue_details(self.pr_json)
55
+ self.ignored_violations = ignored_violations_from_pr(self.pr_json)
56
+ end
51
57
 
52
- issues = client.issue_comments(ci_source.repo_slug, ci_source.pull_request_id)
53
- editable_issues = issues.reject { |issue| issue[:body].include?("generated_by_danger") == false }
58
+ def ignored_violations_from_pr(pr_json)
59
+ pr_body = pr_json[:body]
60
+ return [] if pr_body.nil?
61
+ pr_body.chomp.scan(/>\s*danger\s*:\s*ignore\s*"(.*)"/i).flatten
62
+ end
54
63
 
55
- if editable_issues.empty?
56
- previous_violations = {}
57
- else
58
- comment = editable_issues.first[:body]
59
- previous_violations = parse_comment(comment)
64
+ def fetch_issue_details(pr_json)
65
+ href = pr_json[:_links][:issue][:href]
66
+ self.issue_json = client.get(href)
60
67
  end
61
68
 
62
- if previous_violations.empty? && (warnings + errors + messages + markdowns).empty?
63
- # Just remove the comment, if there's nothing to say.
64
- delete_old_comments!
65
- else
66
- body = generate_comment(warnings: warnings,
67
- errors: errors,
68
- messages: messages,
69
- markdowns: markdowns,
70
- previous_violations: previous_violations)
69
+ # Sending data to GitHub
70
+ def update_pull_request!(warnings: [], errors: [], messages: [], markdowns: [], danger_id: 'danger')
71
+ comment_result = {}
72
+
73
+ issues = client.issue_comments(ci_source.repo_slug, ci_source.pull_request_id)
74
+ editable_issues = issues.reject { |issue| issue[:body].include?("generated_by_#{danger_id}") == false }
71
75
 
72
76
  if editable_issues.empty?
73
- comment_result = client.add_comment(ci_source.repo_slug, ci_source.pull_request_id, body)
77
+ previous_violations = {}
74
78
  else
75
- original_id = editable_issues.first[:id]
76
- comment_result = client.update_comment(ci_source.repo_slug, original_id, body)
79
+ comment = editable_issues.first[:body]
80
+ previous_violations = parse_comment(comment)
77
81
  end
78
- end
79
82
 
80
- # Now, set the pull request status.
81
- # Note: this can terminate the entire process.
82
- submit_pull_request_status!(warnings: warnings,
83
+ if previous_violations.empty? && (warnings + errors + messages + markdowns).empty?
84
+ # Just remove the comment, if there's nothing to say.
85
+ delete_old_comments!(danger_id: danger_id)
86
+ else
87
+ body = generate_comment(warnings: warnings,
83
88
  errors: errors,
84
- details_url: comment_result['html_url'])
85
- end
89
+ messages: messages,
90
+ markdowns: markdowns,
91
+ previous_violations: previous_violations,
92
+ danger_id: danger_id)
93
+
94
+ if editable_issues.empty?
95
+ comment_result = client.add_comment(ci_source.repo_slug, ci_source.pull_request_id, body)
96
+ else
97
+ original_id = editable_issues.first[:id]
98
+ comment_result = client.update_comment(ci_source.repo_slug, original_id, body)
99
+ end
100
+ end
86
101
 
87
- def submit_pull_request_status!(warnings: nil, errors: nil, details_url: nil)
88
- status = (errors.count == 0 ? 'success' : 'failure')
89
- message = generate_github_description(warnings: warnings, errors: errors)
90
- client.create_status(ci_source.repo_slug, latest_pr_commit_ref, status, {
91
- description: message,
92
- context: "danger/danger",
93
- target_url: details_url
94
- })
95
- rescue
96
- # This usually means the user has no commit access to this repo
97
- # That's always the case for open source projects where you can only
98
- # use a read-only GitHub account
99
- if errors.count > 0
100
- # We need to fail the actual build here
101
- abort("\nDanger has failed this build. \nFound #{'error'.danger_pluralize(errors.count)} and I don't have write access to the PR set a PR status.")
102
- else
103
- puts message
102
+ # Now, set the pull request status.
103
+ # Note: this can terminate the entire process.
104
+ submit_pull_request_status!(warnings: warnings,
105
+ errors: errors,
106
+ details_url: comment_result['html_url'])
104
107
  end
105
- end
106
108
 
107
- # Get rid of the previously posted comment, to only have the latest one
108
- def delete_old_comments!(except: nil)
109
- issues = client.issue_comments(ci_source.repo_slug, ci_source.pull_request_id)
110
- issues.each do |issue|
111
- next unless issue[:body].include?("generated_by_danger")
112
- next if issue[:id] == except
113
- client.delete_comment(ci_source.repo_slug, issue[:id])
109
+ def submit_pull_request_status!(warnings: nil, errors: nil, details_url: nil)
110
+ status = (errors.count == 0 ? 'success' : 'failure')
111
+ message = generate_github_description(warnings: warnings, errors: errors)
112
+ client.create_status(ci_source.repo_slug, latest_pr_commit_ref, status, {
113
+ description: message,
114
+ context: "danger/danger",
115
+ target_url: details_url
116
+ })
117
+ rescue
118
+ # This usually means the user has no commit access to this repo
119
+ # That's always the case for open source projects where you can only
120
+ # use a read-only GitHub account
121
+ if errors.count > 0
122
+ # We need to fail the actual build here
123
+ abort("\nDanger has failed this build. \nFound #{'error'.danger_pluralize(errors.count)} and I don't have write access to the PR set a PR status.")
124
+ else
125
+ puts message
126
+ end
114
127
  end
115
- end
116
128
 
117
- def random_compliment
118
- compliment = ["Well done.", "Congrats.", "Woo!",
119
- "Yay.", "Jolly good show.", "Good on 'ya.", "Nice work."]
120
- compliment.sample
121
- end
122
-
123
- def generate_github_description(warnings: nil, errors: nil)
124
- if errors.empty? && warnings.empty?
125
- return "All green. #{random_compliment}"
126
- else
127
- message = "⚠ "
128
- message += "#{'Error'.danger_pluralize(errors.count)}. " unless errors.empty?
129
- message += "#{'Warning'.danger_pluralize(warnings.count)}. " unless warnings.empty?
130
- message += "Don't worry, everything is fixable."
131
- return message
129
+ # Get rid of the previously posted comment, to only have the latest one
130
+ def delete_old_comments!(except: nil, danger_id: 'danger')
131
+ issues = client.issue_comments(ci_source.repo_slug, ci_source.pull_request_id)
132
+ issues.each do |issue|
133
+ next unless issue[:body].include?("generated_by_#{danger_id}")
134
+ next if issue[:id] == except
135
+ client.delete_comment(ci_source.repo_slug, issue[:id])
136
+ end
132
137
  end
133
- end
134
138
 
135
- def generate_comment(warnings: [], errors: [], messages: [], markdowns: [], previous_violations: {})
136
- require 'erb'
139
+ def random_compliment
140
+ compliment = ["Well done.", "Congrats.", "Woo!",
141
+ "Yay.", "Jolly good show.", "Good on 'ya.", "Nice work."]
142
+ compliment.sample
143
+ end
137
144
 
138
- md_template = File.join(Danger.gem_path, "lib/danger/comment_generators/github.md.erb")
145
+ def generate_github_description(warnings: nil, errors: nil)
146
+ if errors.empty? && warnings.empty?
147
+ return "All green. #{random_compliment}"
148
+ else
149
+ message = "⚠ "
150
+ message += "#{'Error'.danger_pluralize(errors.count)}. " unless errors.empty?
151
+ message += "#{'Warning'.danger_pluralize(warnings.count)}. " unless warnings.empty?
152
+ message += "Don't worry, everything is fixable."
153
+ return message
154
+ end
155
+ end
139
156
 
140
- # erb: http://www.rrn.dk/rubys-erb-templating-system
141
- # for the extra args: http://stackoverflow.com/questions/4632879/erb-template-removing-the-trailing-line
142
- @tables = [
143
- table("Error", "no_entry_sign", errors, previous_violations),
144
- table("Warning", "warning", warnings, previous_violations),
145
- table("Message", "book", messages, previous_violations)
146
- ]
147
- @markdowns = markdowns
157
+ def generate_comment(warnings: [], errors: [], messages: [], markdowns: [], previous_violations: {}, danger_id: 'danger')
158
+ require 'erb'
148
159
 
149
- return ERB.new(File.read(md_template), 0, "-").result(binding)
150
- end
160
+ md_template = File.join(Danger.gem_path, "lib/danger/comment_generators/github.md.erb")
151
161
 
152
- def table(name, emoji, violations, all_previous_violations)
153
- content = violations.map { |v| process_markdown(v) }
154
- kind = table_kind_from_title(name)
155
- previous_violations = all_previous_violations[kind] || []
156
- messages = content.map(&:message)
157
- resolved_violations = previous_violations.reject { |s| messages.include? s }
158
- count = content.count
159
- { name: name, emoji: emoji, content: content, resolved: resolved_violations, count: count }
160
- end
162
+ # erb: http://www.rrn.dk/rubys-erb-templating-system
163
+ # for the extra args: http://stackoverflow.com/questions/4632879/erb-template-removing-the-trailing-line
164
+ @tables = [
165
+ table("Error", "no_entry_sign", errors, previous_violations),
166
+ table("Warning", "warning", warnings, previous_violations),
167
+ table("Message", "book", messages, previous_violations)
168
+ ]
169
+ @markdowns = markdowns
170
+ @danger_id = danger_id
161
171
 
162
- def parse_comment(comment)
163
- tables = parse_tables_from_comment(comment)
164
- violations = {}
165
- tables.each do |table|
166
- next unless table =~ %r{<th width="100%"(.*?)</th>}im
167
- title = Regexp.last_match(1)
168
- kind = table_kind_from_title(title)
169
- next unless kind
172
+ return ERB.new(File.read(md_template), 0, "-").result(binding)
173
+ end
170
174
 
171
- violations[kind] = violations_from_table(table)
175
+ def table(name, emoji, violations, all_previous_violations)
176
+ content = violations.map { |v| process_markdown(v) }.uniq
177
+ kind = table_kind_from_title(name)
178
+ previous_violations = all_previous_violations[kind] || []
179
+ messages = content.map(&:message)
180
+ resolved_violations = previous_violations.uniq - messages
181
+ count = content.count
182
+ { name: name, emoji: emoji, content: content, resolved: resolved_violations, count: count }
172
183
  end
173
184
 
174
- violations.reject { |_, v| v.empty? }
175
- end
185
+ def parse_comment(comment)
186
+ tables = parse_tables_from_comment(comment)
187
+ violations = {}
188
+ tables.each do |table|
189
+ next unless table =~ %r{<th width="100%"(.*?)</th>}im
190
+ title = Regexp.last_match(1)
191
+ kind = table_kind_from_title(title)
192
+ next unless kind
176
193
 
177
- def violations_from_table(table)
178
- regex = %r{<td data-sticky="true">(?:<del>)?(.*?)(?:</del>)?\s*</td>}im
179
- table.scan(regex).flatten.map(&:strip)
180
- end
194
+ violations[kind] = violations_from_table(table)
195
+ end
181
196
 
182
- def table_kind_from_title(title)
183
- if title =~ /error/i
184
- :error
185
- elsif title =~ /warning/i
186
- :warning
187
- elsif title =~ /message/i
188
- :message
197
+ violations.reject { |_, v| v.empty? }
189
198
  end
190
- end
191
199
 
192
- def parse_tables_from_comment(comment)
193
- comment.split('</table>')
194
- end
200
+ def violations_from_table(table)
201
+ regex = %r{<td data-sticky="true">(?:<del>)?(.*?)(?:</del>)?\s*</td>}im
202
+ table.scan(regex).flatten.map(&:strip)
203
+ end
195
204
 
196
- def process_markdown(violation)
197
- html = markdown_parser.render(violation.message)
198
- match = html.match(%r{^<p>(.*)</p>$})
199
- message = match.nil? ? html : match.captures.first
200
- Violation.new(message, violation.sticky)
205
+ def table_kind_from_title(title)
206
+ if title =~ /error/i
207
+ :error
208
+ elsif title =~ /warning/i
209
+ :warning
210
+ elsif title =~ /message/i
211
+ :message
212
+ end
213
+ end
214
+
215
+ def parse_tables_from_comment(comment)
216
+ comment.split('</table>')
217
+ end
218
+
219
+ def process_markdown(violation)
220
+ html = markdown_parser.render(violation.message)
221
+ match = html.match(%r{^<p>(.*)</p>$})
222
+ message = match.nil? ? html : match.captures.first
223
+ Violation.new(message, violation.sticky)
224
+ end
201
225
  end
202
226
  end
203
227
  end
@@ -0,0 +1,48 @@
1
+ module Danger
2
+ module RequestSources
3
+ class RequestSource
4
+ attr_accessor :ci_source, :environment, :scm, :host, :ignored_violations
5
+
6
+ def self.inherited(child_class)
7
+ available_request_sources.add child_class
8
+ super
9
+ end
10
+
11
+ def self.available_request_sources
12
+ @available_request_sources ||= Set.new
13
+ end
14
+
15
+ def initialize(_ci_source, _environment)
16
+ raise "Subclass and overwrite initialize"
17
+ end
18
+
19
+ def validates?
20
+ !!self.scm.origins.match(%r{#{Regexp.escape self.host}(:|/)(?<repo_slug>.+/.+?)(?:\.git)?$})
21
+ end
22
+
23
+ def scm
24
+ @scm ||= nil
25
+ end
26
+
27
+ def host
28
+ @host ||= nil
29
+ end
30
+
31
+ def ignored_violations
32
+ @ignored_violations ||= []
33
+ end
34
+
35
+ def update_pull_request!(_warnings: [], _errors: [], _messages: [], _markdowns: [])
36
+ raise "Subclass and overwrite update_pull_request!"
37
+ end
38
+
39
+ def setup_danger_branches
40
+ raise "Subclass and overwrite setup_danger_branches"
41
+ end
42
+
43
+ def fetch_details
44
+ raise "Subclass and overwrite initialize"
45
+ end
46
+ end
47
+ end
48
+ end
@@ -15,5 +15,9 @@ module Danger
15
15
  def exec(string)
16
16
  `git #{string}`.strip
17
17
  end
18
+
19
+ def origins
20
+ exec "remote show origin -n | grep \"Fetch URL\" | cut -d ':' -f 2-"
21
+ end
18
22
  end
19
23
  end
@@ -1,4 +1,4 @@
1
1
  module Danger
2
- VERSION = "0.8.1".freeze
2
+ VERSION = "0.8.2".freeze
3
3
  DESCRIPTION = "Automate your PR etiquette.".freeze
4
4
  end
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.8.1
4
+ version: 0.8.2
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-06-02 00:00:00.000000000 Z
12
+ date: 2016-06-09 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: claide
@@ -273,6 +273,7 @@ files:
273
273
  - lib/danger/ci_source/jenkins.rb
274
274
  - lib/danger/ci_source/local_git_repo.rb
275
275
  - lib/danger/ci_source/semaphore.rb
276
+ - lib/danger/ci_source/teamcity.rb
276
277
  - lib/danger/ci_source/travis.rb
277
278
  - lib/danger/ci_source/xcode_server.rb
278
279
  - lib/danger/commands/init.rb
@@ -295,6 +296,7 @@ files:
295
296
  - lib/danger/plugin_support/plugin.rb
296
297
  - lib/danger/plugin_support/plugin_parser.rb
297
298
  - lib/danger/request_source/github.rb
299
+ - lib/danger/request_source/request_source.rb
298
300
  - lib/danger/scm_source/git_repo.rb
299
301
  - lib/danger/version.rb
300
302
  homepage: http://github.com/danger/danger