gitlab-dangerfiles 1.0.0 → 2.1.1

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__)
@@ -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.0.0"
3
+ VERSION = "2.1.1"
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.0.0
4
+ version: 2.1.1
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,311 +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
- }.freeze
23
-
24
- HTTPError = Class.new(StandardError)
25
-
26
- def gitlab_helper
27
- # Unfortunately the following does not work:
28
- # - respond_to?(:gitlab)
29
- # - respond_to?(:gitlab, true)
30
- gitlab
31
- rescue NoMethodError
32
- nil
33
- end
34
-
35
- def html_link(str)
36
- ci? ? gitlab_helper.html_link(str) : str
37
- end
38
-
39
- def ci?
40
- !gitlab_helper.nil?
41
- end
42
-
43
- # @param [String] url
44
- def http_get_json(url)
45
- rsp = Net::HTTP.get_response(URI.parse(url))
46
-
47
- unless rsp.is_a?(Net::HTTPOK)
48
- raise HTTPError, "Failed to read #{url}: #{rsp.code} #{rsp.message}"
49
- end
50
-
51
- JSON.parse(rsp.body)
52
- end
53
-
54
- def added_files
55
- @added_files ||= if changes_from_api
56
- changes_from_api.select { |file| file["new_file"] }.map { |file| file["new_path"] }
57
- else
58
- git.added_files.to_a
59
- end
60
- end
61
-
62
- def modified_files
63
- @modified_files ||= if changes_from_api
64
- changes_from_api.select { |file| !file["new_file"] && !file["deleted_file"] && !file["renamed_file"] }.map { |file| file["new_path"] }
65
- else
66
- git.modified_files.to_a
67
- end
68
- end
69
-
70
- def renamed_files
71
- @renamed_files ||= if changes_from_api
72
- changes_from_api.select { |file| file["renamed_file"] }.each_with_object([]) do |file, memo|
73
- memo << { before: file["old_path"], after: file["new_path"] }
74
- end
75
- else
76
- git.renamed_files.to_a
77
- end
78
- end
79
-
80
- def deleted_files
81
- @deleted_files ||= if changes_from_api
82
- changes_from_api.select { |file| file["deleted_file"] }.map { |file| file["new_path"] }
83
- else
84
- git.deleted_files.to_a
85
- end
86
- end
87
-
88
- def diff_for_file(filename)
89
- if changes_from_api
90
- changes_hash = changes_from_api.find { |file| file["new_path"] == filename }
91
- changes_hash["diff"] if changes_hash
92
- else
93
- git.diff_for_file(filename)&.patch
94
- end
95
- end
96
-
97
- def changes_from_api
98
- return nil unless ci?
99
- return nil if defined?(@force_changes_from_git)
100
-
101
- @changes_from_api ||= gitlab_helper.api.merge_request_changes(gitlab_helper.mr_json["project_id"], gitlab_helper.mr_json["iid"]).to_h["changes"]
102
- rescue
103
- # Fallback to the Git strategy in any case
104
- @force_changes_from_git = true
105
- nil
106
- end
107
-
108
- # Returns a list of all files that have been added, modified or renamed.
109
- # `modified_files` might contain paths that already have been renamed,
110
- # so we need to remove them from the list.
111
- #
112
- # Considering these changes:
113
- #
114
- # - A new_file.rb
115
- # - D deleted_file.rb
116
- # - M modified_file.rb
117
- # - R renamed_file_before.rb -> renamed_file_after.rb
118
- #
119
- # it will return
120
- # ```
121
- # [ 'new_file.rb', 'modified_file.rb', 'renamed_file_after.rb' ]
122
- # ```
123
- #
124
- # @return [Array<String>]
125
- def all_changed_files
126
- Set.new
127
- .merge(added_files)
128
- .merge(modified_files)
129
- .merge(renamed_files.map { |x| x[:after] })
130
- .subtract(renamed_files.map { |x| x[:before] })
131
- .to_a
132
- .sort
133
- end
134
-
135
- # Returns a string containing changed lines as git diff
136
- #
137
- # Considering changing a line in lib/gitlab/usage_data.rb it will return:
138
- #
139
- # [ "--- a/lib/gitlab/usage_data.rb",
140
- # "+++ b/lib/gitlab/usage_data.rb",
141
- # "+ # Test change",
142
- # "- # Old change" ]
143
- def changed_lines(changed_file)
144
- diff = diff_for_file(changed_file)
145
- return [] unless diff
146
-
147
- diff.split("\n").select { |line| %r{^[+-]}.match?(line) }
148
- end
149
-
150
- def release_automation?
151
- gitlab_helper&.mr_author == RELEASE_TOOLS_BOT
152
- end
153
-
154
- def markdown_list(items)
155
- list = items.map { |item| "* `#{item}`" }.join("\n")
156
-
157
- if items.size > 10
158
- "\n<details>\n\n#{list}\n\n</details>\n"
159
- else
160
- list
161
- end
162
- end
163
-
164
- # @return [Hash<Symbol,Array<String>>]
165
- def changes_by_category(categories)
166
- all_changed_files.each_with_object(Hash.new { |h, k| h[k] = [] }) do |file, hash|
167
- categories_for_file(file, categories).each { |category| hash[category] << file }
168
- end
169
- end
170
-
171
- # @return [Gitlab::Dangerfiles::Changes]
172
- def changes(categories)
173
- Gitlab::Dangerfiles::Changes.new([]).tap do |changes|
174
- added_files.each do |file|
175
- categories_for_file(file, categories).each { |category| changes << Gitlab::Dangerfiles::Change.new(file, :added, category) }
176
- end
177
-
178
- modified_files.each do |file|
179
- categories_for_file(file, categories).each { |category| changes << Gitlab::Dangerfiles::Change.new(file, :modified, category) }
180
- end
181
-
182
- deleted_files.each do |file|
183
- categories_for_file(file, categories).each { |category| changes << Gitlab::Dangerfiles::Change.new(file, :deleted, category) }
184
- end
185
-
186
- renamed_files.map { |x| x[:before] }.each do |file|
187
- categories_for_file(file, categories).each { |category| changes << Gitlab::Dangerfiles::Change.new(file, :renamed_before, category) }
188
- end
189
-
190
- renamed_files.map { |x| x[:after] }.each do |file|
191
- categories_for_file(file, categories).each { |category| changes << Gitlab::Dangerfiles::Change.new(file, :renamed_after, category) }
192
- end
193
- end
194
- end
195
-
196
- # Determines the categories a file is in, e.g., `[:frontend]`, `[:backend]`, or `%i[frontend engineering_productivity]`
197
- # using filename regex and specific change regex if given.
198
- #
199
- # @return Array<Symbol>
200
- def categories_for_file(file, categories)
201
- _, categories = categories.find do |key, _|
202
- filename_regex, changes_regex = Array(key)
203
-
204
- found = filename_regex.match?(file)
205
- found &&= changed_lines(file).any? { |changed_line| changes_regex.match?(changed_line) } if changes_regex
206
-
207
- found
208
- end
209
-
210
- Array(categories || :unknown)
211
- end
212
-
213
- # Returns the GFM for a category label, making its best guess if it's not
214
- # a category we know about.
215
- #
216
- # @return[String]
217
- def label_for_category(category)
218
- CATEGORY_LABELS.fetch(category, "~#{category}")
219
- end
220
-
221
- def new_teammates(usernames)
222
- usernames.map { |u| Gitlab::Dangerfiles::Teammate.new("username" => u) }
223
- end
224
-
225
- def mr_iid
226
- return "" unless ci?
227
-
228
- gitlab_helper.mr_json["iid"]
229
- end
230
-
231
- def mr_title
232
- return "" unless ci?
233
-
234
- gitlab_helper.mr_json["title"]
235
- end
236
-
237
- def mr_web_url
238
- return "" unless ci?
239
-
240
- gitlab_helper.mr_json["web_url"]
241
- end
242
-
243
- def mr_labels
244
- return [] unless ci?
245
-
246
- gitlab_helper.mr_labels
247
- end
248
-
249
- def mr_target_branch
250
- return "" unless ci?
251
-
252
- gitlab_helper.mr_json["target_branch"]
253
- end
254
-
255
- def draft_mr?
256
- Gitlab::Dangerfiles::TitleLinting.has_draft_flag?(mr_title)
257
- end
258
-
259
- def security_mr?
260
- mr_web_url.include?("/gitlab-org/security/")
261
- end
262
-
263
- def cherry_pick_mr?
264
- Gitlab::Dangerfiles::TitleLinting.has_cherry_pick_flag?(mr_title)
265
- end
266
-
267
- def run_all_rspec_mr?
268
- Gitlab::Dangerfiles::TitleLinting.has_run_all_rspec_flag?(mr_title)
269
- end
270
-
271
- def run_as_if_foss_mr?
272
- Gitlab::Dangerfiles::TitleLinting.has_run_as_if_foss_flag?(mr_title)
273
- end
274
-
275
- def stable_branch?
276
- /\A\d+-\d+-stable-ee/i.match?(mr_target_branch)
277
- end
278
-
279
- def mr_has_labels?(*labels)
280
- labels = labels.flatten.uniq
281
-
282
- (labels & mr_labels) == labels
283
- end
284
-
285
- def labels_list(labels, sep: ", ")
286
- labels.map { |label| %Q{~"#{label}"} }.join(sep)
287
- end
288
-
289
- def prepare_labels_for_mr(labels)
290
- return "" unless labels.any?
291
-
292
- "/label #{labels_list(labels, sep: " ")}"
293
- end
294
-
295
- def changed_files(regex)
296
- all_changed_files.grep(regex)
297
- end
298
-
299
- def has_database_scoped_labels?(current_mr_labels)
300
- current_mr_labels.any? { |label| label.start_with?("database::") }
301
- end
302
-
303
- def has_ci_changes?
304
- changed_files(%r{\A(\.gitlab-ci\.yml|\.gitlab/ci/)}).any?
305
- end
306
-
307
- def group_label(labels)
308
- labels.find { |label| label.start_with?("group::") }
309
- end
310
- end
311
- end