gitlab-dangerfiles 1.1.0 → 2.1.2
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 +4 -4
- data/.gitignore +1 -0
- data/.gitlab-ci.yml +36 -1
- data/.yardopts +5 -0
- data/Gemfile +1 -0
- data/LICENSE.txt +1 -1
- data/README.md +57 -6
- data/gitlab-dangerfiles.gemspec +3 -3
- data/lib/danger/plugins/helper.rb +426 -0
- data/lib/danger/{roulette.rb → plugins/roulette.rb} +80 -63
- data/lib/danger/rules/changes_size/Dangerfile +10 -0
- data/lib/danger/rules/commit_messages/Dangerfile +138 -0
- data/lib/gitlab/dangerfiles.rb +82 -2
- data/lib/gitlab/dangerfiles/changes.rb +22 -0
- data/lib/gitlab/dangerfiles/commit_linter.rb +7 -3
- data/lib/gitlab/dangerfiles/config.rb +23 -0
- data/lib/gitlab/dangerfiles/emoji_checker.rb +1 -0
- data/lib/gitlab/dangerfiles/teammate.rb +0 -5
- data/lib/gitlab/dangerfiles/title_linting.rb +2 -0
- data/lib/gitlab/dangerfiles/version.rb +1 -1
- data/lib/gitlab/dangerfiles/weightage.rb +1 -0
- data/lib/gitlab/dangerfiles/weightage/maintainers.rb +1 -0
- data/lib/gitlab/dangerfiles/weightage/reviewers.rb +1 -0
- metadata +12 -8
- data/lib/danger/helper.rb +0 -312
@@ -1,9 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "
|
4
|
-
require_relative "
|
5
|
-
require_relative "
|
6
|
-
require_relative "../gitlab/dangerfiles/weightage/reviewers"
|
3
|
+
require_relative "../../gitlab/dangerfiles/teammate"
|
4
|
+
require_relative "../../gitlab/dangerfiles/weightage/maintainers"
|
5
|
+
require_relative "../../gitlab/dangerfiles/weightage/reviewers"
|
7
6
|
|
8
7
|
module Danger
|
9
8
|
# Common helper functions for our danger scripts. See Danger::Helper
|
@@ -17,13 +16,21 @@ module Danger
|
|
17
16
|
}.freeze
|
18
17
|
|
19
18
|
Spin = Struct.new(:category, :reviewer, :maintainer, :optional_role, :timezone_experiment)
|
19
|
+
HTTPError = Class.new(StandardError)
|
20
20
|
|
21
|
+
# Finds the +Gitlab::Dangerfiles::Teammate+ object whose username matches the MR author username.
|
22
|
+
#
|
23
|
+
# @return [Gitlab::Dangerfiles::Teammate]
|
21
24
|
def team_mr_author
|
22
|
-
|
25
|
+
company_members.find { |person| person.username == helper.mr_author }
|
23
26
|
end
|
24
27
|
|
25
28
|
# Assigns GitLab team members to be reviewer and maintainer
|
26
|
-
# for
|
29
|
+
# for the given +categories+.
|
30
|
+
#
|
31
|
+
# @param project [String] A project path.
|
32
|
+
# @param categories [Array<Symbol>] An array of categories symbols.
|
33
|
+
# @param timezone_experiment [Boolean] Whether to select reviewers based in timezone or not.
|
27
34
|
#
|
28
35
|
# @return [Array<Spin>]
|
29
36
|
def spin(project, categories = [nil], timezone_experiment: false)
|
@@ -34,6 +41,7 @@ module Danger
|
|
34
41
|
end
|
35
42
|
|
36
43
|
backend_spin = spins.find { |spin| spin.category == :backend }
|
44
|
+
frontend_spin = spins.find { |spin| spin.category == :frontend }
|
37
45
|
|
38
46
|
spins.each do |spin|
|
39
47
|
including_timezone = INCLUDE_TIMEZONE_FOR_CATEGORY.fetch(spin.category, timezone_experiment)
|
@@ -62,81 +70,35 @@ module Danger
|
|
62
70
|
end
|
63
71
|
when :product_intelligence
|
64
72
|
spin.optional_role = :maintainer
|
73
|
+
|
74
|
+
if spin.maintainer.nil?
|
75
|
+
# Fetch an already picked maintainer, or pick one otherwise
|
76
|
+
spin.maintainer = backend_spin&.maintainer || frontend_spin&.maintainer || spin_for_category(project, :backend, timezone_experiment: including_timezone).maintainer
|
77
|
+
end
|
65
78
|
end
|
66
79
|
end
|
67
80
|
|
68
81
|
spins
|
69
82
|
end
|
70
83
|
|
71
|
-
# Looks up the current list of GitLab team members and parses it into a
|
72
|
-
# useful form
|
73
|
-
#
|
74
|
-
# @return [Array<Teammate>]
|
75
|
-
def team
|
76
|
-
@team ||= begin
|
77
|
-
data = helper.http_get_json(ROULETTE_DATA_URL)
|
78
|
-
data.map { |hash| Gitlab::Dangerfiles::Teammate.new(hash) }
|
79
|
-
rescue JSON::ParserError
|
80
|
-
raise "Failed to parse JSON response from #{ROULETTE_DATA_URL}"
|
81
|
-
end
|
82
|
-
end
|
83
|
-
|
84
|
-
# Like +team+, but only returns teammates in the current project, based on
|
85
|
-
# project_name.
|
86
|
-
#
|
87
|
-
# @return [Array<Teammate>]
|
88
|
-
def project_team(project_name)
|
89
|
-
team.select { |member| member.in_project?(project_name) }
|
90
|
-
rescue => err
|
91
|
-
warn("Reviewer roulette failed to load team data: #{err.message}")
|
92
|
-
[]
|
93
|
-
end
|
94
|
-
|
95
|
-
# Known issue: If someone is rejected due to OOO, and then becomes not OOO, the
|
96
|
-
# selection will change on next spin
|
97
|
-
# @param [Array<Teammate>] people
|
98
|
-
def spin_for_person(people, random:, timezone_experiment: false)
|
99
|
-
shuffled_people = people.shuffle(random: random)
|
100
|
-
|
101
|
-
if timezone_experiment
|
102
|
-
shuffled_people.find(&method(:valid_person_with_timezone?))
|
103
|
-
else
|
104
|
-
shuffled_people.find(&method(:valid_person?))
|
105
|
-
end
|
106
|
-
end
|
107
|
-
|
108
84
|
private
|
109
85
|
|
110
|
-
# @param [Teammate] person
|
86
|
+
# @param [Gitlab::Dangerfiles::Teammate] person
|
111
87
|
# @return [Boolean]
|
112
88
|
def valid_person?(person)
|
113
89
|
!mr_author?(person) && person.available
|
114
90
|
end
|
115
91
|
|
116
|
-
# @param [Teammate] person
|
92
|
+
# @param [Gitlab::Dangerfiles::Teammate] person
|
117
93
|
# @return [Boolean]
|
118
94
|
def valid_person_with_timezone?(person)
|
119
95
|
valid_person?(person) && HOURS_WHEN_PERSON_CAN_BE_PICKED.cover?(person.local_hour)
|
120
96
|
end
|
121
97
|
|
122
|
-
# @param [Teammate] person
|
98
|
+
# @param [Gitlab::Dangerfiles::Teammate] person
|
123
99
|
# @return [Boolean]
|
124
100
|
def mr_author?(person)
|
125
|
-
person.username ==
|
126
|
-
end
|
127
|
-
|
128
|
-
def mr_author_username
|
129
|
-
helper.gitlab_helper&.mr_author || `whoami`
|
130
|
-
end
|
131
|
-
|
132
|
-
def mr_source_branch
|
133
|
-
return `git rev-parse --abbrev-ref HEAD` unless helper.gitlab_helper&.mr_json
|
134
|
-
|
135
|
-
helper.gitlab_helper.mr_json["source_branch"]
|
136
|
-
end
|
137
|
-
|
138
|
-
def mr_labels
|
139
|
-
helper.gitlab_helper&.mr_labels || []
|
101
|
+
person.username == helper.mr_author
|
140
102
|
end
|
141
103
|
|
142
104
|
def new_random(seed)
|
@@ -145,7 +107,23 @@ module Danger
|
|
145
107
|
|
146
108
|
def spin_role_for_category(team, role, project, category)
|
147
109
|
team.select do |member|
|
148
|
-
member.public_send("#{role}?", project, category, mr_labels) # rubocop:disable GitlabSecurity/PublicSend
|
110
|
+
member.public_send("#{role}?", project, category, helper.mr_labels) # rubocop:disable GitlabSecurity/PublicSend
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Known issue: If someone is rejected due to OOO, and then becomes not OOO, the
|
115
|
+
# selection will change on next spin.
|
116
|
+
#
|
117
|
+
# @param [Array<Gitlab::Dangerfiles::Teammate>] people
|
118
|
+
#
|
119
|
+
# @return [Gitlab::Dangerfiles::Teammate]
|
120
|
+
def spin_for_person(people, random:, timezone_experiment: false)
|
121
|
+
shuffled_people = people.shuffle(random: random)
|
122
|
+
|
123
|
+
if timezone_experiment
|
124
|
+
shuffled_people.find(&method(:valid_person_with_timezone?))
|
125
|
+
else
|
126
|
+
shuffled_people.find(&method(:valid_person?))
|
149
127
|
end
|
150
128
|
end
|
151
129
|
|
@@ -156,7 +134,7 @@ module Danger
|
|
156
134
|
spin_role_for_category(team, role, project, category)
|
157
135
|
end
|
158
136
|
|
159
|
-
random = new_random(mr_source_branch)
|
137
|
+
random = new_random(helper.mr_source_branch)
|
160
138
|
|
161
139
|
weighted_reviewers = Gitlab::Dangerfiles::Weightage::Reviewers.new(reviewers, traintainers).execute
|
162
140
|
weighted_maintainers = Gitlab::Dangerfiles::Weightage::Maintainers.new(maintainers).execute
|
@@ -166,5 +144,44 @@ module Danger
|
|
166
144
|
|
167
145
|
Spin.new(category, reviewer, maintainer, false, timezone_experiment)
|
168
146
|
end
|
147
|
+
|
148
|
+
# Fetches the given +url+ and parse its response as JSON.
|
149
|
+
#
|
150
|
+
# @param [String] url
|
151
|
+
#
|
152
|
+
# @return [Hash, Array]
|
153
|
+
def http_get_json(url)
|
154
|
+
rsp = Net::HTTP.get_response(URI.parse(url))
|
155
|
+
|
156
|
+
unless rsp.is_a?(Net::HTTPOK)
|
157
|
+
raise HTTPError, "Failed to read #{url}: #{rsp.code} #{rsp.message}"
|
158
|
+
end
|
159
|
+
|
160
|
+
JSON.parse(rsp.body)
|
161
|
+
end
|
162
|
+
|
163
|
+
# Looks up the current list of GitLab team members and parses it into a
|
164
|
+
# useful form.
|
165
|
+
#
|
166
|
+
# @return [Array<Gitlab::Dangerfiles::Teammate>]
|
167
|
+
def company_members
|
168
|
+
@company_members ||= begin
|
169
|
+
data = http_get_json(ROULETTE_DATA_URL)
|
170
|
+
data.map { |hash| Gitlab::Dangerfiles::Teammate.new(hash) }
|
171
|
+
rescue JSON::ParserError
|
172
|
+
raise "Failed to parse JSON response from #{ROULETTE_DATA_URL}"
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# Like +team+, but only returns teammates in the current project, based on
|
177
|
+
# project_name.
|
178
|
+
#
|
179
|
+
# @return [Array<Gitlab::Dangerfiles::Teammate>]
|
180
|
+
def project_team(project_name)
|
181
|
+
company_members.select { |member| member.in_project?(project_name) }
|
182
|
+
rescue => err
|
183
|
+
warn("Reviewer roulette failed to load team data: #{err.message}")
|
184
|
+
[]
|
185
|
+
end
|
169
186
|
end
|
170
187
|
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
thresholds = helper.config.code_size_thresholds
|
4
|
+
lines_changed = git.lines_of_code
|
5
|
+
|
6
|
+
if lines_changed > thresholds[:high]
|
7
|
+
warn "This merge request is definitely too big (#{lines_changed} lines changed), please split it into multiple merge requests."
|
8
|
+
elsif lines_changed > thresholds[:medium]
|
9
|
+
warn "This merge request is quite big (#{lines_changed} lines changed), please consider splitting it into multiple merge requests."
|
10
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../../../gitlab/dangerfiles/commit_linter"
|
4
|
+
require_relative "../../../gitlab/dangerfiles/merge_request_linter"
|
5
|
+
|
6
|
+
COMMIT_MESSAGE_GUIDELINES = "https://docs.gitlab.com/ee/development/contributing/merge_request_workflow.html#commit-messages-guidelines"
|
7
|
+
MORE_INFO = "For more information, take a look at our [Commit message guidelines](#{COMMIT_MESSAGE_GUIDELINES})."
|
8
|
+
THE_DANGER_JOB_TEXT = "the `%<job_name>` job"
|
9
|
+
MAX_COMMITS_COUNT = helper.config.max_commits_count
|
10
|
+
MAX_COMMITS_COUNT_EXCEEDED_MESSAGE = <<~MSG
|
11
|
+
This merge request includes more than %<max_commits_count>d commits. Each commit should meet the following criteria:
|
12
|
+
|
13
|
+
1. Have a well-written commit message.
|
14
|
+
1. Has all tests passing when used on its own (e.g. when using git checkout SHA).
|
15
|
+
1. Can be reverted on its own without also requiring the revert of commit that came before it.
|
16
|
+
1. Is small enough that it can be reviewed in isolation in under 30 minutes or so.
|
17
|
+
|
18
|
+
If this merge request contains commits that do not meet this criteria and/or contains intermediate work, please rebase these commits into a smaller number of commits or split this merge request into multiple smaller merge requests.
|
19
|
+
MSG
|
20
|
+
|
21
|
+
def fail_commit(commit, message, more_info: true)
|
22
|
+
self.fail(build_message(commit, message, more_info: more_info))
|
23
|
+
end
|
24
|
+
|
25
|
+
def warn_commit(commit, message, more_info: true)
|
26
|
+
self.warn(build_message(commit, message, more_info: more_info))
|
27
|
+
end
|
28
|
+
|
29
|
+
def build_message(commit, message, more_info: true)
|
30
|
+
[message].tap do |full_message|
|
31
|
+
full_message << ". #{MORE_INFO}" if more_info
|
32
|
+
full_message.unshift("#{commit.sha}: ") if commit.sha
|
33
|
+
end.join
|
34
|
+
end
|
35
|
+
|
36
|
+
def danger_job_link
|
37
|
+
helper.ci? ? "[#{format(THE_DANGER_JOB_TEXT, job_name: ENV["CI_JOB_NAME"])}](#{ENV['CI_JOB_URL']})" : THE_DANGER_JOB_TEXT
|
38
|
+
end
|
39
|
+
|
40
|
+
# Perform various checks against commits. We're not using
|
41
|
+
# https://github.com/jonallured/danger-commit_lint because its output is not
|
42
|
+
# very helpful, and it doesn't offer the means of ignoring merge commits.
|
43
|
+
def lint_commit(commit)
|
44
|
+
linter = Gitlab::Dangerfiles::CommitLinter.new(commit)
|
45
|
+
|
46
|
+
# For now we'll ignore merge commits, as getting rid of those is a problem
|
47
|
+
# separate from enforcing good commit messages.
|
48
|
+
return linter if linter.merge?
|
49
|
+
|
50
|
+
# We ignore revert commits as they are well structured by Git already
|
51
|
+
return linter if linter.revert?
|
52
|
+
|
53
|
+
# If MR is set to squash, we ignore fixup commits
|
54
|
+
return linter if linter.fixup? && helper.squash_mr?
|
55
|
+
|
56
|
+
if linter.fixup?
|
57
|
+
msg = "Squash or fixup commits must be squashed before merge, or enable squash merge option and re-run #{danger_job_link}."
|
58
|
+
if helper.draft_mr? || helper.squash_mr?
|
59
|
+
warn_commit(commit, msg, more_info: false)
|
60
|
+
else
|
61
|
+
fail_commit(commit, msg, more_info: false)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Makes no sense to process other rules for fixup commits, they trigger just more noise
|
65
|
+
return linter
|
66
|
+
end
|
67
|
+
|
68
|
+
# Fail if a suggestion commit is used and squash is not enabled
|
69
|
+
if linter.suggestion?
|
70
|
+
unless helper.squash_mr?
|
71
|
+
fail_commit(commit, "If you are applying suggestions, enable squash in the merge request and re-run #{danger_job_link}.", more_info: false)
|
72
|
+
end
|
73
|
+
|
74
|
+
return linter
|
75
|
+
end
|
76
|
+
|
77
|
+
linter.lint
|
78
|
+
end
|
79
|
+
|
80
|
+
def lint_mr_title(mr_title)
|
81
|
+
commit = Struct.new(:message, :sha).new(mr_title)
|
82
|
+
|
83
|
+
Gitlab::Dangerfiles::MergeRequestLinter.new(commit).lint
|
84
|
+
end
|
85
|
+
|
86
|
+
def count_non_fixup_commits(commit_linters)
|
87
|
+
commit_linters.count { |commit_linter| !commit_linter.fixup? }
|
88
|
+
end
|
89
|
+
|
90
|
+
def lint_commits(commits)
|
91
|
+
commit_linters = commits.map { |commit| lint_commit(commit) }
|
92
|
+
|
93
|
+
if helper.squash_mr?
|
94
|
+
multi_line_commit_linter = commit_linters.detect { |commit_linter| !commit_linter.merge? && commit_linter.multi_line? }
|
95
|
+
|
96
|
+
if multi_line_commit_linter && multi_line_commit_linter.failed?
|
97
|
+
warn_or_fail_commits(multi_line_commit_linter)
|
98
|
+
commit_linters.delete(multi_line_commit_linter) # Don't show an error (here) and a warning (below)
|
99
|
+
elsif helper.ci? # We don't have access to the MR title locally
|
100
|
+
title_linter = lint_mr_title(gitlab.mr_json['title'])
|
101
|
+
if title_linter.failed?
|
102
|
+
warn_or_fail_commits(title_linter)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
else
|
106
|
+
if count_non_fixup_commits(commit_linters) > MAX_COMMITS_COUNT
|
107
|
+
self.warn(format(MAX_COMMITS_COUNT_EXCEEDED_MESSAGE, max_commits_count: MAX_COMMITS_COUNT))
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
failed_commit_linters = commit_linters.select { |commit_linter| commit_linter.failed? }
|
112
|
+
warn_or_fail_commits(failed_commit_linters, default_to_fail: !helper.squash_mr?)
|
113
|
+
end
|
114
|
+
|
115
|
+
def warn_or_fail_commits(failed_linters, default_to_fail: true)
|
116
|
+
level = default_to_fail ? :fail : :warn
|
117
|
+
|
118
|
+
Array(failed_linters).each do |linter|
|
119
|
+
linter.problems.each do |problem_key, problem_desc|
|
120
|
+
case problem_key
|
121
|
+
when :subject_too_short, :subject_above_warning, :details_too_many_changes, :details_line_too_long
|
122
|
+
warn_commit(linter.commit, problem_desc)
|
123
|
+
else
|
124
|
+
self.__send__("#{level}_commit", linter.commit, problem_desc) # rubocop:disable GitlabSecurity/PublicSend
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# As part of https://gitlab.com/groups/gitlab-org/-/epics/4826 we are
|
131
|
+
# vendoring workhorse commits from the stand-alone gitlab-workhorse
|
132
|
+
# repo. There is no point in linting commits that we want to vendor as
|
133
|
+
# is.
|
134
|
+
def workhorse_changes?
|
135
|
+
git.diff.any? { |file| file.path.start_with?('workhorse/') }
|
136
|
+
end
|
137
|
+
|
138
|
+
lint_commits(git.commits) unless workhorse_changes?
|
data/lib/gitlab/dangerfiles.rb
CHANGED
@@ -2,8 +2,88 @@ require "gitlab/dangerfiles/version"
|
|
2
2
|
|
3
3
|
module Gitlab
|
4
4
|
module Dangerfiles
|
5
|
-
|
6
|
-
|
5
|
+
RULES_DIR = File.expand_path("../danger/rules", __dir__)
|
6
|
+
EXISTING_RULES = Dir.glob(File.join(RULES_DIR, "*")).each_with_object([]) do |path, memo|
|
7
|
+
if File.directory?(path)
|
8
|
+
memo << File.basename(path)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
LOCAL_RULES = %w[
|
12
|
+
changes_size
|
13
|
+
commit_messages
|
14
|
+
].freeze
|
15
|
+
CI_ONLY_RULES = %w[
|
16
|
+
].freeze
|
17
|
+
|
18
|
+
# This class provides utility methods to import plugins and dangerfiles easily.
|
19
|
+
class Engine
|
20
|
+
# @param dangerfile [Danger::Dangerfile] A +Danger::Dangerfile+ object.
|
21
|
+
#
|
22
|
+
# @example
|
23
|
+
# # In your main Dangerfile:
|
24
|
+
# dangerfiles = Gitlab::Dangerfiles::Engine.new(self)
|
25
|
+
#
|
26
|
+
# @return [Gitlab::Dangerfiles::Engine]
|
27
|
+
def initialize(dangerfile)
|
28
|
+
@dangerfile = dangerfile
|
29
|
+
end
|
30
|
+
|
31
|
+
# Import all available plugins.
|
32
|
+
#
|
33
|
+
# @example
|
34
|
+
# # In your main Dangerfile:
|
35
|
+
# dangerfiles = Gitlab::Dangerfiles::Engine.new(self)
|
36
|
+
#
|
37
|
+
# # Import all plugins
|
38
|
+
# dangerfiles.import_plugins
|
39
|
+
def import_plugins
|
40
|
+
danger_plugin.import_plugin(File.expand_path("../danger/plugins/*.rb", __dir__))
|
41
|
+
end
|
42
|
+
|
43
|
+
# Import available Dangerfiles.
|
44
|
+
#
|
45
|
+
# @param rules [Symbol, Array<String>] Can be either +:all+ (default) to import all rules,
|
46
|
+
# or an array of rules.
|
47
|
+
# Available rules are: +changes_size+.
|
48
|
+
#
|
49
|
+
# @example
|
50
|
+
# # In your main Dangerfile:
|
51
|
+
# dangerfiles = Gitlab::Dangerfiles::Engine.new(self)
|
52
|
+
#
|
53
|
+
# # Import all rules
|
54
|
+
# dangerfiles.import_dangerfiles
|
55
|
+
#
|
56
|
+
# # Or import only a subset of rules
|
57
|
+
# dangerfiles.import_dangerfiles(rules: %w[changes_size])
|
58
|
+
def import_dangerfiles(rules: :all)
|
59
|
+
filtered_rules(rules).each do |rule|
|
60
|
+
danger_plugin.import_dangerfile(path: File.join(RULES_DIR, rule))
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
attr_reader :dangerfile
|
67
|
+
|
68
|
+
def allowed_rules
|
69
|
+
return LOCAL_RULES unless helper_plugin.respond_to?(:ci?)
|
70
|
+
|
71
|
+
helper_plugin.ci? ? LOCAL_RULES | CI_ONLY_RULES : LOCAL_RULES
|
72
|
+
end
|
73
|
+
|
74
|
+
def filtered_rules(rules)
|
75
|
+
rules = EXISTING_RULES if rules == :all
|
76
|
+
|
77
|
+
Array(rules).map(&:to_s) & EXISTING_RULES & allowed_rules
|
78
|
+
end
|
79
|
+
|
80
|
+
def danger_plugin
|
81
|
+
@danger_plugin ||= dangerfile.plugins[Danger::DangerfileDangerPlugin]
|
82
|
+
end
|
83
|
+
|
84
|
+
def helper_plugin
|
85
|
+
@helper_plugin ||= dangerfile.plugins[Danger::Helper]
|
86
|
+
end
|
7
87
|
end
|
8
88
|
end
|
9
89
|
end
|