gitlab-dangerfiles 1.1.0 → 2.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,41 +4,63 @@ require_relative "title_linting"
4
4
 
5
5
  module Gitlab
6
6
  module Dangerfiles
7
+ # @!attribute file
8
+ # @return [String] the file name that's changed.
9
+ # @!attribute change_type
10
+ # @return [Symbol] the type of change (+:added+, +:modified+, +:deleted+, +:renamed_before+, +:renamed_after+).
11
+ # @!attribute category
12
+ # @return [Symbol] the category of the change.
13
+ # This is defined by consumers of the gem through +helper.changes_by_category+ or +helper.changes+.
7
14
  Change = Struct.new(:file, :change_type, :category)
8
15
 
9
16
  class Changes < ::SimpleDelegator
17
+ # Return an +Gitlab::Dangerfiles::Changes+ object with only the changes for the added files.
18
+ #
19
+ # @return [Gitlab::Dangerfiles::Changes]
10
20
  def added
11
21
  select_by_change_type(:added)
12
22
  end
13
23
 
24
+ # @return [Gitlab::Dangerfiles::Changes] the changes for the modified files.
14
25
  def modified
15
26
  select_by_change_type(:modified)
16
27
  end
17
28
 
29
+ # @return [Gitlab::Dangerfiles::Changes] the changes for the deleted files.
18
30
  def deleted
19
31
  select_by_change_type(:deleted)
20
32
  end
21
33
 
34
+ # @return [Gitlab::Dangerfiles::Changes] the changes for the renamed files (before the rename).
22
35
  def renamed_before
23
36
  select_by_change_type(:renamed_before)
24
37
  end
25
38
 
39
+ # @return [Gitlab::Dangerfiles::Changes] the changes for the renamed files (after the rename).
26
40
  def renamed_after
27
41
  select_by_change_type(:renamed_after)
28
42
  end
29
43
 
44
+ # @param category [Symbol] A category of change.
45
+ #
46
+ # @return [Boolean] whether there are any change for the given +category+.
30
47
  def has_category?(category)
31
48
  any? { |change| change.category == category }
32
49
  end
33
50
 
51
+ # @param category [Symbol] a category of change.
52
+ #
53
+ # @return [Gitlab::Dangerfiles::Changes] changes for the given +category+.
34
54
  def by_category(category)
35
55
  Changes.new(select { |change| change.category == category })
36
56
  end
37
57
 
58
+ # @return [Array<Symbol>] an array of the unique categories of changes.
38
59
  def categories
39
60
  map(&:category).uniq
40
61
  end
41
62
 
63
+ # @return [Array<String>] an array of the changed files.
42
64
  def files
43
65
  map(&:file)
44
66
  end
@@ -8,14 +8,17 @@ module Gitlab
8
8
  class CommitLinter < BaseLinter
9
9
  MAX_CHANGED_FILES_IN_COMMIT = 3
10
10
  MAX_CHANGED_LINES_IN_COMMIT = 30
11
- SHORT_REFERENCE_REGEX = %r{([\w\-\/]+)?(?<!`)(#|!|&|%)\d+(?<!`)}.freeze
11
+ # Issue, MR, Epic
12
+ SHORT_REFERENCE_REGEX = %r{([\w\-\/]+)?(?<!`)(#|!|&)\d+(?<!`)}.freeze
13
+ # Milestone
14
+ MS_SHORT_REFERENCE_REGEX = %r{([\w\-\/]+)?(?<!`)%"?\d{1,3}\.\d{1,3}"?(?<!`)}.freeze
12
15
 
13
16
  def self.problems_mapping
14
17
  super.merge(
15
18
  {
16
19
  separator_missing: "The commit subject and body must be separated by a blank line",
17
20
  details_too_many_changes: "Commits that change #{MAX_CHANGED_LINES_IN_COMMIT} or more lines across " \
18
- "at least #{MAX_CHANGED_FILES_IN_COMMIT} files must describe these changes in the commit body",
21
+ "at least #{MAX_CHANGED_FILES_IN_COMMIT} files should describe these changes in the commit body",
19
22
  details_line_too_long: "The commit body should not contain more than #{MAX_LINE_LENGTH} characters per line",
20
23
  message_contains_text_emoji: "Avoid the use of Markdown Emoji such as `:+1:`. These add limited value " \
21
24
  "to the commit message, and are displayed as plain text outside of GitLab",
@@ -139,7 +142,8 @@ module Gitlab
139
142
  end
140
143
 
141
144
  def message_contains_short_reference?
142
- commit.message.match?(SHORT_REFERENCE_REGEX)
145
+ commit.message.match?(SHORT_REFERENCE_REGEX) ||
146
+ commit.message.match?(MS_SHORT_REFERENCE_REGEX)
143
147
  end
144
148
 
145
149
  def emoji_checker
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gitlab
4
+ module Dangerfiles
5
+ class Config
6
+ # @!attribute code_size_thresholds
7
+ # @return [{ high: Integer, medium: Integer }] a hash of the form +{ high: 42, medium: 12 }+ where +:high+ is the lines changed threshold which triggers an error, and +:medium+ is the lines changed threshold which triggers a warning. Also, see +DEFAULT_CHANGES_SIZE_THRESHOLDS+ for the format of the hash.
8
+ attr_accessor :code_size_thresholds
9
+
10
+ # @!attribute max_commits_count
11
+ # @return [Integer] the maximum number of allowed non-squashed/non-fixup commits for a given MR. A warning is triggered if the MR has more commits.
12
+ attr_accessor :max_commits_count
13
+
14
+ DEFAULT_CHANGES_SIZE_THRESHOLDS = { high: 2_000, medium: 500 }.freeze
15
+ DEFAULT_COMMIT_MESSAGES_MAX_COMMITS_COUNT = 10
16
+
17
+ def initialize
18
+ @code_size_thresholds = DEFAULT_CHANGES_SIZE_THRESHOLDS
19
+ @max_commits_count = DEFAULT_COMMIT_MESSAGES_MAX_COMMITS_COUNT
20
+ end
21
+ end
22
+ end
23
+ end
@@ -4,6 +4,7 @@ require "json"
4
4
 
5
5
  module Gitlab
6
6
  module Dangerfiles
7
+ # @api private
7
8
  class EmojiChecker
8
9
  DIGESTS = File.expand_path("../../../fixtures/emojis/digests.json", __dir__)
9
10
  ALIASES = File.expand_path("../../../fixtures/emojis/aliases.json", __dir__)
@@ -101,11 +101,6 @@ module Gitlab
101
101
  return true if capabilities(project).include?("#{kind} engineering_productivity")
102
102
 
103
103
  capabilities(project).include?("#{kind} backend")
104
- when :product_intelligence
105
- return false unless role[/Engineer, Product Intelligence/]
106
-
107
- # any reviewer of project from Product Intelligence team can review MR
108
- kind == :reviewer && capabilities(project).any?
109
104
  when nil
110
105
  capabilities(project).include?("#{kind}")
111
106
  else
@@ -19,6 +19,8 @@ module Gitlab
19
19
  end
20
20
 
21
21
  def has_draft_flag?(title)
22
+ puts "This method is deprecated in favor of `helper.draft_mr?`."
23
+
22
24
  DRAFT_REGEX.match?(title)
23
25
  end
24
26
 
@@ -1,5 +1,5 @@
1
1
  module Gitlab
2
2
  module Dangerfiles
3
- VERSION = "1.1.0"
3
+ VERSION = "2.1.2"
4
4
  end
5
5
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Gitlab
4
4
  module Dangerfiles
5
+ # @api private
5
6
  module Weightage
6
7
  CAPACITY_MULTIPLIER = 2 # change this number to change what it means to be a reduced capacity reviewer 1/this number
7
8
  BASE_REVIEWER_WEIGHT = 1
@@ -5,6 +5,7 @@ require_relative "../weightage"
5
5
  module Gitlab
6
6
  module Dangerfiles
7
7
  module Weightage
8
+ # @api private
8
9
  class Maintainers
9
10
  def initialize(maintainers)
10
11
  @maintainers = maintainers
@@ -18,6 +18,7 @@ module Gitlab
18
18
  # | hungry traintainer | 8 |
19
19
  # +------------------------------+--------------------------------+
20
20
  #
21
+ # @api private
21
22
  class Reviewers
22
23
  DEFAULT_REVIEWER_WEIGHT = Gitlab::Dangerfiles::Weightage::CAPACITY_MULTIPLIER * Gitlab::Dangerfiles::Weightage::BASE_REVIEWER_WEIGHT
23
24
  TRAINTAINER_WEIGHT = 3
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gitlab-dangerfiles
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 2.1.2
5
5
  platform: ruby
6
6
  authors:
7
- - Rémy Coutable
7
+ - GitLab
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-03-25 00:00:00.000000000 Z
11
+ date: 2021-06-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: danger-gitlab
@@ -110,7 +110,7 @@ dependencies:
110
110
  version: '0'
111
111
  description: This gem provides common Dangerfile and plugins for GitLab projects.
112
112
  email:
113
- - remy@rymai.me
113
+ - gitlab_rubygems@gitlab.com
114
114
  executables: []
115
115
  extensions: []
116
116
  extra_rdoc_files: []
@@ -119,6 +119,7 @@ files:
119
119
  - ".gitlab-ci.yml"
120
120
  - ".gitlab/merge_request_templates/Release.md"
121
121
  - ".rspec"
122
+ - ".yardopts"
122
123
  - CODE_OF_CONDUCT.md
123
124
  - Gemfile
124
125
  - Guardfile
@@ -130,14 +131,17 @@ files:
130
131
  - fixtures/emojis/aliases.json
131
132
  - fixtures/emojis/digests.json
132
133
  - gitlab-dangerfiles.gemspec
133
- - lib/danger/helper.rb
134
- - lib/danger/roulette.rb
134
+ - lib/danger/plugins/helper.rb
135
+ - lib/danger/plugins/roulette.rb
136
+ - lib/danger/rules/changes_size/Dangerfile
137
+ - lib/danger/rules/commit_messages/Dangerfile
135
138
  - lib/gitlab-dangerfiles.rb
136
139
  - lib/gitlab/Dangerfile
137
140
  - lib/gitlab/dangerfiles.rb
138
141
  - lib/gitlab/dangerfiles/base_linter.rb
139
142
  - lib/gitlab/dangerfiles/changes.rb
140
143
  - lib/gitlab/dangerfiles/commit_linter.rb
144
+ - lib/gitlab/dangerfiles/config.rb
141
145
  - lib/gitlab/dangerfiles/emoji_checker.rb
142
146
  - lib/gitlab/dangerfiles/merge_request_linter.rb
143
147
  - lib/gitlab/dangerfiles/spec_helper.rb
@@ -154,7 +158,7 @@ metadata:
154
158
  allowed_push_host: https://rubygems.org
155
159
  homepage_uri: https://gitlab.com/gitlab-org/gitlab-dangerfiles
156
160
  source_code_uri: https://gitlab.com/gitlab-org/gitlab-dangerfiles
157
- changelog_uri: https://gitlab.com/gitlab-org/gitlab-dangerfiles
161
+ changelog_uri: https://gitlab.com/gitlab-org/gitlab-dangerfiles/-/releases
158
162
  post_install_message:
159
163
  rdoc_options: []
160
164
  require_paths:
@@ -170,7 +174,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
170
174
  - !ruby/object:Gem::Version
171
175
  version: '0'
172
176
  requirements: []
173
- rubygems_version: 3.1.4
177
+ rubygems_version: 3.1.6
174
178
  signing_key:
175
179
  specification_version: 4
176
180
  summary: This gem provides common Dangerfile and plugins for GitLab projects.
data/lib/danger/helper.rb DELETED
@@ -1,312 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "net/http"
4
- require "json"
5
- require "danger"
6
- require_relative "../gitlab/dangerfiles/changes"
7
- require_relative "../gitlab/dangerfiles/teammate"
8
- require_relative "../gitlab/dangerfiles/title_linting"
9
-
10
- module Danger
11
- # Common helper functions for our danger scripts.
12
- class Helper < Danger::Plugin
13
- RELEASE_TOOLS_BOT = "gitlab-release-tools-bot"
14
- DRAFT_REGEX = /\A*#{Regexp.union(/(?i)(\[WIP\]\s*|WIP:\s*|WIP$)/, /(?i)(\[draft\]|\(draft\)|draft:|draft\s\-\s|draft$)/)}+\s*/i.freeze
15
- CATEGORY_LABELS = {
16
- docs: "~documentation", # Docs are reviewed along DevOps stages, so don't need roulette for now.
17
- none: "",
18
- qa: "~QA",
19
- test: "~test ~Quality for `spec/features/*`",
20
- engineering_productivity: '~"Engineering Productivity" for CI, Danger',
21
- ci_template: '~"ci::templates"',
22
- product_intelligence: '~"product intelligence"',
23
- }.freeze
24
-
25
- HTTPError = Class.new(StandardError)
26
-
27
- def gitlab_helper
28
- # Unfortunately the following does not work:
29
- # - respond_to?(:gitlab)
30
- # - respond_to?(:gitlab, true)
31
- gitlab
32
- rescue NoMethodError
33
- nil
34
- end
35
-
36
- def html_link(str)
37
- ci? ? gitlab_helper.html_link(str) : str
38
- end
39
-
40
- def ci?
41
- !gitlab_helper.nil?
42
- end
43
-
44
- # @param [String] url
45
- def http_get_json(url)
46
- rsp = Net::HTTP.get_response(URI.parse(url))
47
-
48
- unless rsp.is_a?(Net::HTTPOK)
49
- raise HTTPError, "Failed to read #{url}: #{rsp.code} #{rsp.message}"
50
- end
51
-
52
- JSON.parse(rsp.body)
53
- end
54
-
55
- def added_files
56
- @added_files ||= if changes_from_api
57
- changes_from_api.select { |file| file["new_file"] }.map { |file| file["new_path"] }
58
- else
59
- git.added_files.to_a
60
- end
61
- end
62
-
63
- def modified_files
64
- @modified_files ||= if changes_from_api
65
- changes_from_api.select { |file| !file["new_file"] && !file["deleted_file"] && !file["renamed_file"] }.map { |file| file["new_path"] }
66
- else
67
- git.modified_files.to_a
68
- end
69
- end
70
-
71
- def renamed_files
72
- @renamed_files ||= if changes_from_api
73
- changes_from_api.select { |file| file["renamed_file"] }.each_with_object([]) do |file, memo|
74
- memo << { before: file["old_path"], after: file["new_path"] }
75
- end
76
- else
77
- git.renamed_files.to_a
78
- end
79
- end
80
-
81
- def deleted_files
82
- @deleted_files ||= if changes_from_api
83
- changes_from_api.select { |file| file["deleted_file"] }.map { |file| file["new_path"] }
84
- else
85
- git.deleted_files.to_a
86
- end
87
- end
88
-
89
- def diff_for_file(filename)
90
- if changes_from_api
91
- changes_hash = changes_from_api.find { |file| file["new_path"] == filename }
92
- changes_hash["diff"] if changes_hash
93
- else
94
- git.diff_for_file(filename)&.patch
95
- end
96
- end
97
-
98
- def changes_from_api
99
- return nil unless ci?
100
- return nil if defined?(@force_changes_from_git)
101
-
102
- @changes_from_api ||= gitlab_helper.api.merge_request_changes(gitlab_helper.mr_json["project_id"], gitlab_helper.mr_json["iid"]).to_h["changes"]
103
- rescue
104
- # Fallback to the Git strategy in any case
105
- @force_changes_from_git = true
106
- nil
107
- end
108
-
109
- # Returns a list of all files that have been added, modified or renamed.
110
- # `modified_files` might contain paths that already have been renamed,
111
- # so we need to remove them from the list.
112
- #
113
- # Considering these changes:
114
- #
115
- # - A new_file.rb
116
- # - D deleted_file.rb
117
- # - M modified_file.rb
118
- # - R renamed_file_before.rb -> renamed_file_after.rb
119
- #
120
- # it will return
121
- # ```
122
- # [ 'new_file.rb', 'modified_file.rb', 'renamed_file_after.rb' ]
123
- # ```
124
- #
125
- # @return [Array<String>]
126
- def all_changed_files
127
- Set.new
128
- .merge(added_files)
129
- .merge(modified_files)
130
- .merge(renamed_files.map { |x| x[:after] })
131
- .subtract(renamed_files.map { |x| x[:before] })
132
- .to_a
133
- .sort
134
- end
135
-
136
- # Returns a string containing changed lines as git diff
137
- #
138
- # Considering changing a line in lib/gitlab/usage_data.rb it will return:
139
- #
140
- # [ "--- a/lib/gitlab/usage_data.rb",
141
- # "+++ b/lib/gitlab/usage_data.rb",
142
- # "+ # Test change",
143
- # "- # Old change" ]
144
- def changed_lines(changed_file)
145
- diff = diff_for_file(changed_file)
146
- return [] unless diff
147
-
148
- diff.split("\n").select { |line| %r{^[+-]}.match?(line) }
149
- end
150
-
151
- def release_automation?
152
- gitlab_helper&.mr_author == RELEASE_TOOLS_BOT
153
- end
154
-
155
- def markdown_list(items)
156
- list = items.map { |item| "* `#{item}`" }.join("\n")
157
-
158
- if items.size > 10
159
- "\n<details>\n\n#{list}\n\n</details>\n"
160
- else
161
- list
162
- end
163
- end
164
-
165
- # @return [Hash<Symbol,Array<String>>]
166
- def changes_by_category(categories)
167
- all_changed_files.each_with_object(Hash.new { |h, k| h[k] = [] }) do |file, hash|
168
- categories_for_file(file, categories).each { |category| hash[category] << file }
169
- end
170
- end
171
-
172
- # @return [Gitlab::Dangerfiles::Changes]
173
- def changes(categories)
174
- Gitlab::Dangerfiles::Changes.new([]).tap do |changes|
175
- added_files.each do |file|
176
- categories_for_file(file, categories).each { |category| changes << Gitlab::Dangerfiles::Change.new(file, :added, category) }
177
- end
178
-
179
- modified_files.each do |file|
180
- categories_for_file(file, categories).each { |category| changes << Gitlab::Dangerfiles::Change.new(file, :modified, category) }
181
- end
182
-
183
- deleted_files.each do |file|
184
- categories_for_file(file, categories).each { |category| changes << Gitlab::Dangerfiles::Change.new(file, :deleted, category) }
185
- end
186
-
187
- renamed_files.map { |x| x[:before] }.each do |file|
188
- categories_for_file(file, categories).each { |category| changes << Gitlab::Dangerfiles::Change.new(file, :renamed_before, category) }
189
- end
190
-
191
- renamed_files.map { |x| x[:after] }.each do |file|
192
- categories_for_file(file, categories).each { |category| changes << Gitlab::Dangerfiles::Change.new(file, :renamed_after, category) }
193
- end
194
- end
195
- end
196
-
197
- # Determines the categories a file is in, e.g., `[:frontend]`, `[:backend]`, or `%i[frontend engineering_productivity]`
198
- # using filename regex and specific change regex if given.
199
- #
200
- # @return Array<Symbol>
201
- def categories_for_file(file, categories)
202
- _, categories = categories.find do |key, _|
203
- filename_regex, changes_regex = Array(key)
204
-
205
- found = filename_regex.match?(file)
206
- found &&= changed_lines(file).any? { |changed_line| changes_regex.match?(changed_line) } if changes_regex
207
-
208
- found
209
- end
210
-
211
- Array(categories || :unknown)
212
- end
213
-
214
- # Returns the GFM for a category label, making its best guess if it's not
215
- # a category we know about.
216
- #
217
- # @return[String]
218
- def label_for_category(category)
219
- CATEGORY_LABELS.fetch(category, "~#{category}")
220
- end
221
-
222
- def new_teammates(usernames)
223
- usernames.map { |u| Gitlab::Dangerfiles::Teammate.new("username" => u) }
224
- end
225
-
226
- def mr_iid
227
- return "" unless ci?
228
-
229
- gitlab_helper.mr_json["iid"]
230
- end
231
-
232
- def mr_title
233
- return "" unless ci?
234
-
235
- gitlab_helper.mr_json["title"]
236
- end
237
-
238
- def mr_web_url
239
- return "" unless ci?
240
-
241
- gitlab_helper.mr_json["web_url"]
242
- end
243
-
244
- def mr_labels
245
- return [] unless ci?
246
-
247
- gitlab_helper.mr_labels
248
- end
249
-
250
- def mr_target_branch
251
- return "" unless ci?
252
-
253
- gitlab_helper.mr_json["target_branch"]
254
- end
255
-
256
- def draft_mr?
257
- Gitlab::Dangerfiles::TitleLinting.has_draft_flag?(mr_title)
258
- end
259
-
260
- def security_mr?
261
- mr_web_url.include?("/gitlab-org/security/")
262
- end
263
-
264
- def cherry_pick_mr?
265
- Gitlab::Dangerfiles::TitleLinting.has_cherry_pick_flag?(mr_title)
266
- end
267
-
268
- def run_all_rspec_mr?
269
- Gitlab::Dangerfiles::TitleLinting.has_run_all_rspec_flag?(mr_title)
270
- end
271
-
272
- def run_as_if_foss_mr?
273
- Gitlab::Dangerfiles::TitleLinting.has_run_as_if_foss_flag?(mr_title)
274
- end
275
-
276
- def stable_branch?
277
- /\A\d+-\d+-stable-ee/i.match?(mr_target_branch)
278
- end
279
-
280
- def mr_has_labels?(*labels)
281
- labels = labels.flatten.uniq
282
-
283
- (labels & mr_labels) == labels
284
- end
285
-
286
- def labels_list(labels, sep: ", ")
287
- labels.map { |label| %Q{~"#{label}"} }.join(sep)
288
- end
289
-
290
- def prepare_labels_for_mr(labels)
291
- return "" unless labels.any?
292
-
293
- "/label #{labels_list(labels, sep: " ")}"
294
- end
295
-
296
- def changed_files(regex)
297
- all_changed_files.grep(regex)
298
- end
299
-
300
- def has_database_scoped_labels?(current_mr_labels)
301
- current_mr_labels.any? { |label| label.start_with?("database::") }
302
- end
303
-
304
- def has_ci_changes?
305
- changed_files(%r{\A(\.gitlab-ci\.yml|\.gitlab/ci/)}).any?
306
- end
307
-
308
- def group_label(labels)
309
- labels.find { |label| label.start_with?("group::") }
310
- end
311
- end
312
- end