gitlab-dangerfiles 0.1.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/lib/danger/helper.rb +155 -113
  3. data/lib/danger/roulette.rb +68 -36
  4. data/lib/gitlab/dangerfiles.rb +0 -34
  5. data/lib/gitlab/dangerfiles/base_linter.rb +96 -0
  6. data/lib/gitlab/dangerfiles/commit_linter.rb +25 -103
  7. data/lib/gitlab/dangerfiles/merge_request_linter.rb +30 -0
  8. data/lib/gitlab/dangerfiles/teammate.rb +23 -9
  9. data/lib/gitlab/dangerfiles/title_linting.rb +38 -0
  10. data/lib/gitlab/dangerfiles/version.rb +1 -1
  11. data/lib/gitlab/dangerfiles/weightage.rb +10 -0
  12. data/lib/gitlab/dangerfiles/weightage/maintainers.rb +33 -0
  13. data/lib/gitlab/dangerfiles/weightage/reviewers.rb +65 -0
  14. metadata +9 -25
  15. data/lib/danger/changelog.rb +0 -39
  16. data/lib/danger/sidekiq_queues.rb +0 -37
  17. data/lib/gitlab/dangerfiles/bundle_size/Dangerfile +0 -38
  18. data/lib/gitlab/dangerfiles/ce_ee_vue_templates/Dangerfile +0 -56
  19. data/lib/gitlab/dangerfiles/changelog/Dangerfile +0 -90
  20. data/lib/gitlab/dangerfiles/changes_size/Dangerfile +0 -17
  21. data/lib/gitlab/dangerfiles/commit_messages/Dangerfile +0 -135
  22. data/lib/gitlab/dangerfiles/database/Dangerfile +0 -67
  23. data/lib/gitlab/dangerfiles/documentation/Dangerfile +0 -29
  24. data/lib/gitlab/dangerfiles/duplicate_yarn_dependencies/Dangerfile +0 -29
  25. data/lib/gitlab/dangerfiles/eslint/Dangerfile +0 -31
  26. data/lib/gitlab/dangerfiles/frozen_string/Dangerfile +0 -28
  27. data/lib/gitlab/dangerfiles/karma/Dangerfile +0 -51
  28. data/lib/gitlab/dangerfiles/metadata/Dangerfile +0 -50
  29. data/lib/gitlab/dangerfiles/popen.rb +0 -55
  30. data/lib/gitlab/dangerfiles/prettier/Dangerfile +0 -41
  31. data/lib/gitlab/dangerfiles/roulette/Dangerfile +0 -97
  32. data/lib/gitlab/dangerfiles/sidekiq_queues/Dangerfile +0 -27
  33. data/lib/gitlab/dangerfiles/specs/Dangerfile +0 -42
  34. data/lib/gitlab/dangerfiles/tasks.rb +0 -19
  35. data/lib/gitlab/dangerfiles/telemetry/Dangerfile +0 -32
  36. data/lib/gitlab/dangerfiles/utility_css/Dangerfile +0 -51
@@ -2,39 +2,5 @@ require "gitlab/dangerfiles/version"
2
2
 
3
3
  module Gitlab
4
4
  module Dangerfiles
5
- LOCAL_RULES ||= %w[
6
- changes_size
7
- commit_messages
8
- database
9
- documentation
10
- duplicate_yarn_dependencies
11
- eslint
12
- frozen_string
13
- karma
14
- prettier
15
- telemetry
16
- utility_css
17
- ].freeze
18
- CI_ONLY_RULES ||= %w[
19
- ce_ee_vue_templates
20
- changelog
21
- metadata
22
- roulette
23
- sidekiq_queues
24
- specs
25
- ].freeze
26
- MESSAGE_PREFIX = "==>".freeze
27
-
28
- def self.load_tasks
29
- require "gitlab/dangerfiles/tasks"
30
- end
31
-
32
- def self.local_warning_message
33
- "#{MESSAGE_PREFIX} Only the following Danger rules can be run locally: #{LOCAL_RULES.join(", ")}"
34
- end
35
-
36
- def self.success_message
37
- "#{MESSAGE_PREFIX} No Danger rule violations!"
38
- end
39
5
  end
40
6
  end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "title_linting"
4
+
5
+ module Gitlab
6
+ module Dangerfiles
7
+ class BaseLinter
8
+ MIN_SUBJECT_WORDS_COUNT = 3
9
+ MAX_LINE_LENGTH = 72
10
+
11
+ attr_reader :commit, :problems
12
+
13
+ def self.problems_mapping
14
+ {
15
+ subject_too_short: "The %s must contain at least #{MIN_SUBJECT_WORDS_COUNT} words",
16
+ subject_too_long: "The %s may not be longer than #{MAX_LINE_LENGTH} characters",
17
+ subject_starts_with_lowercase: "The %s must start with a capital letter",
18
+ subject_ends_with_a_period: "The %s must not end with a period",
19
+ }
20
+ end
21
+
22
+ def self.subject_description
23
+ "commit subject"
24
+ end
25
+
26
+ def initialize(commit)
27
+ @commit = commit
28
+ @problems = {}
29
+ end
30
+
31
+ def failed?
32
+ problems.any?
33
+ end
34
+
35
+ def add_problem(problem_key, *args)
36
+ @problems[problem_key] = sprintf(self.class.problems_mapping[problem_key], *args)
37
+ end
38
+
39
+ def lint_subject
40
+ if subject_too_short?
41
+ add_problem(:subject_too_short, self.class.subject_description)
42
+ end
43
+
44
+ if subject_too_long?
45
+ add_problem(:subject_too_long, self.class.subject_description)
46
+ end
47
+
48
+ if subject_starts_with_lowercase?
49
+ add_problem(:subject_starts_with_lowercase, self.class.subject_description)
50
+ end
51
+
52
+ if subject_ends_with_a_period?
53
+ add_problem(:subject_ends_with_a_period, self.class.subject_description)
54
+ end
55
+
56
+ self
57
+ end
58
+
59
+ private
60
+
61
+ def subject
62
+ TitleLinting.remove_draft_flag(message_parts[0])
63
+ end
64
+
65
+ def subject_too_short?
66
+ subject.split(" ").length < MIN_SUBJECT_WORDS_COUNT
67
+ end
68
+
69
+ def subject_too_long?
70
+ line_too_long?(subject)
71
+ end
72
+
73
+ def line_too_long?(line)
74
+ line.length > MAX_LINE_LENGTH
75
+ end
76
+
77
+ def subject_starts_with_lowercase?
78
+ return false if ("A".."Z").cover?(subject[0])
79
+
80
+ first_char = subject.sub(/\A(\[.+\]|\w+:)\s/, "")[0]
81
+ first_char_downcased = first_char.downcase
82
+ return true unless ("a".."z").cover?(first_char_downcased)
83
+
84
+ first_char.downcase == first_char
85
+ end
86
+
87
+ def subject_ends_with_a_period?
88
+ subject.end_with?(".")
89
+ end
90
+
91
+ def message_parts
92
+ @message_parts ||= commit.message.split("\n", 3)
93
+ end
94
+ end
95
+ end
96
+ end
@@ -1,40 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- emoji_checker_path = File.expand_path("emoji_checker", __dir__)
4
- defined?(Rails) ? require_dependency(emoji_checker_path) : require_relative(emoji_checker_path)
3
+ require_relative "emoji_checker"
5
4
 
6
5
  module Gitlab
7
6
  module Dangerfiles
8
7
  class CommitLinter
9
- MIN_SUBJECT_WORDS_COUNT = 3
10
- MAX_LINE_LENGTH = 72
11
- MAX_CHANGED_FILES_IN_COMMIT = 3
12
8
  MAX_CHANGED_LINES_IN_COMMIT = 30
13
- SHORT_REFERENCE_REGEX = %r{([\w\-\/]+)?(#|!|&|%)\d+\b}.freeze
14
- DEFAULT_SUBJECT_DESCRIPTION = "commit subject"
15
- WIP_PREFIX = "WIP: "
16
- PROBLEMS = {
17
- subject_too_short: "The %s must contain at least #{MIN_SUBJECT_WORDS_COUNT} words",
18
- subject_too_long: "The %s may not be longer than #{MAX_LINE_LENGTH} characters",
19
- subject_starts_with_lowercase: "The %s must start with a capital letter",
20
- subject_ends_with_a_period: "The %s must not end with a period",
21
- separator_missing: "The commit subject and body must be separated by a blank line",
22
- details_too_many_changes: "Commits that change #{MAX_CHANGED_LINES_IN_COMMIT} or more lines across " \
23
- "at least #{MAX_CHANGED_FILES_IN_COMMIT} files must describe these changes in the commit body",
24
- details_line_too_long: "The commit body should not contain more than #{MAX_LINE_LENGTH} characters per line",
25
- message_contains_text_emoji: "Avoid the use of Markdown Emoji such as `:+1:`. These add limited value " \
26
- "to the commit message, and are displayed as plain text outside of GitLab",
27
- message_contains_unicode_emoji: "Avoid the use of Unicode Emoji. These add no value to the commit " \
28
- "message, and may not be displayed properly everywhere",
29
- message_contains_short_reference: "Use full URLs instead of short references (`gitlab-org/gitlab#123` or " \
30
- "`!123`), as short references are displayed as plain text outside of GitLab",
31
- }.freeze
32
-
33
- attr_reader :commit, :problems
9
+ SHORT_REFERENCE_REGEX = %r{([\w\-\/]+)?(?<!`)(#|!|&|%)\d+(?<!`)}.freeze
10
+
11
+ def self.problems_mapping
12
+ super.merge(
13
+ {
14
+ separator_missing: "The commit subject and body must be separated by a blank line",
15
+ details_too_many_changes: "Commits that change #{MAX_CHANGED_LINES_IN_COMMIT} or more lines across " \
16
+ "at least #{MAX_CHANGED_FILES_IN_COMMIT} files must describe these changes in the commit body",
17
+ details_line_too_long: "The commit body should not contain more than #{MAX_LINE_LENGTH} characters per line",
18
+ message_contains_text_emoji: "Avoid the use of Markdown Emoji such as `:+1:`. These add limited value " \
19
+ "to the commit message, and are displayed as plain text outside of GitLab",
20
+ message_contains_unicode_emoji: "Avoid the use of Unicode Emoji. These add no value to the commit " \
21
+ "message, and may not be displayed properly everywhere",
22
+ message_contains_short_reference: "Use full URLs instead of short references (`gitlab-org/gitlab#123` or " \
23
+ "`!123`), as short references are displayed as plain text outside of GitLab",
24
+ }
25
+ )
26
+ end
34
27
 
35
28
  def initialize(commit)
36
- @commit = commit
37
- @problems = {}
29
+ super
30
+
38
31
  @linted = false
39
32
  end
40
33
 
@@ -58,19 +51,11 @@ module Gitlab
58
51
  !details.nil? && !details.empty?
59
52
  end
60
53
 
61
- def failed?
62
- problems.any?
63
- end
64
-
65
- def add_problem(problem_key, *args)
66
- @problems[problem_key] = sprintf(PROBLEMS[problem_key], *args)
67
- end
68
-
69
- def lint(subject_description = "commit subject")
54
+ def lint
70
55
  return self if @linted
71
56
 
72
57
  @linted = true
73
- lint_subject(subject_description)
58
+ lint_subject
74
59
  lint_separator
75
60
  lint_details
76
61
  lint_message
@@ -78,26 +63,6 @@ module Gitlab
78
63
  self
79
64
  end
80
65
 
81
- def lint_subject(subject_description)
82
- if subject_too_short?
83
- add_problem(:subject_too_short, subject_description)
84
- end
85
-
86
- if subject_too_long?
87
- add_problem(:subject_too_long, subject_description)
88
- end
89
-
90
- if subject_starts_with_lowercase?
91
- add_problem(:subject_starts_with_lowercase, subject_description)
92
- end
93
-
94
- if subject_ends_with_a_period?
95
- add_problem(:subject_ends_with_a_period, subject_description)
96
- end
97
-
98
- self
99
- end
100
-
101
66
  private
102
67
 
103
68
  def lint_separator
@@ -114,15 +79,11 @@ module Gitlab
114
79
  end
115
80
 
116
81
  details&.each_line do |line|
117
- line = line.strip
118
-
119
- next unless line_too_long?(line)
120
-
121
- url_size = line.scan(%r((https?://\S+))).sum { |(url)| url.length } # rubocop:disable CodeReuse/ActiveRecord
82
+ line_without_urls = line.strip.gsub(%r{https?://\S+}, "")
122
83
 
123
84
  # If the line includes a URL, we'll allow it to exceed MAX_LINE_LENGTH characters, but
124
85
  # only if the line _without_ the URL does not exceed this limit.
125
- next unless line_too_long?(line.length - url_size)
86
+ next unless line_too_long?(line_without_urls)
126
87
 
127
88
  add_problem(:details_line_too_long)
128
89
  break
@@ -159,10 +120,6 @@ module Gitlab
159
120
  files_changed > MAX_CHANGED_FILES_IN_COMMIT && lines_changed > MAX_CHANGED_LINES_IN_COMMIT
160
121
  end
161
122
 
162
- def subject
163
- message_parts[0].delete_prefix(WIP_PREFIX)
164
- end
165
-
166
123
  def separator
167
124
  message_parts[1]
168
125
  end
@@ -171,37 +128,6 @@ module Gitlab
171
128
  message_parts[2]&.gsub(/^Signed-off-by.*$/, "")
172
129
  end
173
130
 
174
- def line_too_long?(line)
175
- case line
176
- when String
177
- line.length > MAX_LINE_LENGTH
178
- when Integer
179
- line > MAX_LINE_LENGTH
180
- else
181
- raise ArgumentError, "The line argument (#{line}) should be a String or an Integer! #{line.class} given."
182
- end
183
- end
184
-
185
- def subject_too_short?
186
- subject.split(" ").length < MIN_SUBJECT_WORDS_COUNT
187
- end
188
-
189
- def subject_too_long?
190
- line_too_long?(subject)
191
- end
192
-
193
- def subject_starts_with_lowercase?
194
- first_char = subject.sub(/\A\[.+\]\s/, "")[0]
195
- first_char_downcased = first_char.downcase
196
- return true unless ("a".."z").cover?(first_char_downcased)
197
-
198
- first_char.downcase == first_char
199
- end
200
-
201
- def subject_ends_with_a_period?
202
- subject.end_with?(".")
203
- end
204
-
205
131
  def message_contains_text_emoji?
206
132
  emoji_checker.includes_text_emoji?(commit.message)
207
133
  end
@@ -217,10 +143,6 @@ module Gitlab
217
143
  def emoji_checker
218
144
  @emoji_checker ||= Gitlab::Dangerfiles::EmojiChecker.new
219
145
  end
220
-
221
- def message_parts
222
- @message_parts ||= commit.message.split("\n", 3)
223
- end
224
146
  end
225
147
  end
226
148
  end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_linter"
4
+
5
+ module Gitlab
6
+ module Dangerfiles
7
+ class MergeRequestLinter < BaseLinter
8
+ alias_method :lint, :lint_subject
9
+
10
+ def self.subject_description
11
+ "merge request title"
12
+ end
13
+
14
+ def self.mr_run_options_regex
15
+ [
16
+ "RUN AS-IF-FOSS",
17
+ "UPDATE CACHE",
18
+ "RUN ALL RSPEC",
19
+ "SKIP RSPEC FAIL-FAST",
20
+ ].join("|")
21
+ end
22
+
23
+ private
24
+
25
+ def subject
26
+ super.gsub(/\[?(#{self.class.mr_run_options_regex})\]?/, "").strip
27
+ end
28
+ end
29
+ end
30
+ end
@@ -3,27 +3,42 @@
3
3
  module Gitlab
4
4
  module Dangerfiles
5
5
  class Teammate
6
- attr_reader :username, :name, :role, :projects, :available, :tz_offset_hours
6
+ attr_reader :options, :username, :name, :role, :projects, :available, :hungry, :reduced_capacity, :tz_offset_hours
7
7
 
8
8
  # The options data are produced by https://gitlab.com/gitlab-org/gitlab-roulette/-/blob/master/lib/team_member.rb
9
9
  def initialize(options = {})
10
+ @options = options
10
11
  @username = options["username"]
11
12
  @name = options["name"]
12
13
  @markdown_name = options["markdown_name"]
13
14
  @role = options["role"]
14
15
  @projects = options["projects"]
15
16
  @available = options["available"]
17
+ @hungry = options["hungry"]
18
+ @reduced_capacity = options["reduced_capacity"]
16
19
  @tz_offset_hours = options["tz_offset_hours"]
17
20
  end
18
21
 
22
+ def to_h
23
+ options
24
+ end
25
+
26
+ def ==(other)
27
+ return false unless other.respond_to?(:username)
28
+
29
+ other.username == username
30
+ end
31
+
19
32
  def in_project?(name)
20
33
  projects&.has_key?(name)
21
34
  end
22
35
 
23
- # Traintainers also count as reviewers
36
+ def any_capability?(project, category)
37
+ capabilities(project).any? { |capability| capability.end_with?(category.to_s) }
38
+ end
39
+
24
40
  def reviewer?(project, category, labels)
25
- has_capability?(project, category, :reviewer, labels) ||
26
- traintainer?(project, category, labels)
41
+ has_capability?(project, category, :reviewer, labels)
27
42
  end
28
43
 
29
44
  def traintainer?(project, category, labels)
@@ -34,9 +49,7 @@ module Gitlab
34
49
  has_capability?(project, category, :maintainer, labels)
35
50
  end
36
51
 
37
- def markdown_name(timezone_experiment: false, author: nil)
38
- return @markdown_name unless timezone_experiment
39
-
52
+ def markdown_name(author: nil)
40
53
  "#{@markdown_name} (#{utc_offset_text(author)})"
41
54
  end
42
55
 
@@ -68,9 +81,9 @@ module Gitlab
68
81
 
69
82
  def offset_diff_compared_to_author(author)
70
83
  diff = floored_offset_hours - author.floored_offset_hours
71
- return "same timezone as `@#{author.username}`" if diff.zero?
84
+ return "same timezone as `@#{author.username}`" if diff == 0
72
85
 
73
- ahead_or_behind = diff < 0 ? "behind" : "ahead"
86
+ ahead_or_behind = diff < 0 ? "behind" : "ahead of"
74
87
  pluralized_hours = pluralize(diff.abs, "hour", "hours")
75
88
 
76
89
  "#{pluralized_hours} #{ahead_or_behind} `@#{author.username}`"
@@ -85,6 +98,7 @@ module Gitlab
85
98
  when :engineering_productivity
86
99
  return false unless role[/Engineering Productivity/]
87
100
  return true if kind == :reviewer
101
+ return true if capabilities(project).include?("#{kind} engineering_productivity")
88
102
 
89
103
  capabilities(project).include?("#{kind} backend")
90
104
  else
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gitlab
4
+ module Dangerfiles
5
+ module TitleLinting
6
+ DRAFT_REGEX = /\A*#{Regexp.union(/(?i)(\[WIP\]\s*|WIP:\s*|WIP$)/, /(?i)(\[draft\]|\(draft\)|draft:|draft\s\-\s|draft$)/)}+\s*/i.freeze
7
+ CHERRY_PICK_REGEX = /cherry[\s-]*pick/i.freeze
8
+ RUN_ALL_RSPEC_REGEX = /RUN ALL RSPEC/i.freeze
9
+ RUN_AS_IF_FOSS_REGEX = /RUN AS-IF-FOSS/i.freeze
10
+
11
+ module_function
12
+
13
+ def sanitize_mr_title(title)
14
+ remove_draft_flag(title).gsub(/`/, '\\\`')
15
+ end
16
+
17
+ def remove_draft_flag(title)
18
+ title.gsub(DRAFT_REGEX, "")
19
+ end
20
+
21
+ def has_draft_flag?(title)
22
+ DRAFT_REGEX.match?(title)
23
+ end
24
+
25
+ def has_cherry_pick_flag?(title)
26
+ CHERRY_PICK_REGEX.match?(title)
27
+ end
28
+
29
+ def has_run_all_rspec_flag?(title)
30
+ RUN_ALL_RSPEC_REGEX.match?(title)
31
+ end
32
+
33
+ def has_run_as_if_foss_flag?(title)
34
+ RUN_AS_IF_FOSS_REGEX.match?(title)
35
+ end
36
+ end
37
+ end
38
+ end