Module: Gitlab::Danger::Helper

Defined in:
lib/gitlab/danger/helper.rb

Constant Summary collapse

RELEASE_TOOLS_BOT =
'gitlab-release-tools-bot'
DRAFT_REGEX =
/\A*#{Regexp.union(/(?i)(\[WIP\]\s*|WIP:\s*|WIP$)/, /(?i)(\[draft\]|\(draft\)|draft:|draft\s\-\s|draft$)/)}+\s*/i.freeze
CATEGORY_LABELS =
{
  docs: "~documentation", # Docs are reviewed along DevOps stages, so don't need roulette for now.
  none: "",
  qa: "~QA",
  test: "~test ~Quality for `spec/features/*`",
  engineering_productivity: '~"Engineering Productivity" for CI, Danger'
}.freeze
CATEGORIES =

First-match win, so be sure to put more specific regex at the top…

{
  [%r{usage_data\.rb}, %r{^(\+|-).*(count|distinct_count)\(.*\)(.*)$}] => [:database, :backend],

  %r{\Adoc/.*(\.(md|png|gif|jpg))\z} => :docs,
  %r{\A(CONTRIBUTING|LICENSE|MAINTENANCE|PHILOSOPHY|PROCESS|README)(\.md)?\z} => :docs,

  %r{\A(ee/)?app/(assets|views)/} => :frontend,
  %r{\A(ee/)?public/} => :frontend,
  %r{\A(ee/)?spec/(javascripts|frontend)/} => :frontend,
  %r{\A(ee/)?vendor/assets/} => :frontend,
  %r{\A(ee/)?scripts/frontend/} => :frontend,
  %r{(\A|/)(
    \.babelrc |
    \.eslintignore |
    \.eslintrc(\.yml)? |
    \.nvmrc |
    \.prettierignore |
    \.prettierrc |
    \.scss-lint.yml |
    \.stylelintrc |
    \.haml-lint.yml |
    \.haml-lint_todo.yml |
    babel\.config\.js |
    jest\.config\.js |
    package\.json |
    yarn\.lock |
    config/.+\.js
  )\z}x => :frontend,

  %r{(\A|/)(
    \.gitlab/ci/frontend\.gitlab-ci\.yml
  )\z}x => %i[frontend engineering_productivity],

  %r{\A(ee/)?db/(?!fixtures)[^/]+} => :database,
  %r{\A(ee/)?lib/gitlab/(database|background_migration|sql|github_import)(/|\.rb)} => :database,
  %r{\A(app/models/project_authorization|app/services/users/refresh_authorized_projects_service)(/|\.rb)} => :database,
  %r{\A(ee/)?app/finders/} => :database,
  %r{\Arubocop/cop/migration(/|\.rb)} => :database,

  %r{\A(\.gitlab-ci\.yml\z|\.gitlab\/ci)} => :engineering_productivity,
  %r{\A\.codeclimate\.yml\z} => :engineering_productivity,
  %r{\A\.overcommit\.yml\.example\z} => :engineering_productivity,
  %r{\A\.editorconfig\z} => :engineering_productivity,
  %r{Dangerfile\z} => :engineering_productivity,
  %r{\A(ee/)?(danger/|lib/gitlab/danger/)} => :engineering_productivity,
  %r{\A(ee/)?scripts/} => :engineering_productivity,
  %r{\Atooling/} => :engineering_productivity,
  %r{(CODEOWNERS)} => :engineering_productivity,
  %r{(tests.yml)} => :engineering_productivity,

  %r{\A(ee/)?spec/features/} => :test,
  %r{\A(ee/)?spec/support/shared_examples/features/} => :test,
  %r{\A(ee/)?spec/support/shared_contexts/features/} => :test,
  %r{\A(ee/)?spec/support/helpers/features/} => :test,

  %r{\A(ee/)?app/(?!assets|views)[^/]+} => :backend,
  %r{\A(ee/)?(bin|config|generator_templates|lib|rubocop)/} => :backend,
  %r{\A(ee/)?spec/} => :backend,
  %r{\A(ee/)?vendor/} => :backend,
  %r{\A(Gemfile|Gemfile.lock|Rakefile)\z} => :backend,
  %r{\A[A-Z_]+_VERSION\z} => :backend,
  %r{\A\.rubocop(_todo)?\.yml\z} => :backend,
  %r{\Afile_hooks/} => :backend,

  %r{\A(ee/)?qa/} => :qa,

  # Files that don't fit into any category are marked with :none
  %r{\A(ee/)?changelogs/} => :none,
  %r{\Alocale/gitlab\.pot\z} => :none,
  %r{\Adata/whats_new/} => :none,

  # Fallbacks in case the above patterns miss anything
  %r{\.rb\z} => :backend,
  %r{(
    \.(md|txt)\z |
    \.markdownlint\.json
  )}x => :none, # To reinstate roulette for documentation, set to `:docs`.
  %r{\.js\z} => :frontend
}.freeze

Instance Method Summary collapse

Instance Method Details

#all_changed_filesArray<String>

Returns a list of all files that have been added, modified or renamed. `git.modified_files` might contain paths that already have been renamed, so we need to remove them from the list.

Considering these changes:

  • A new_file.rb

  • D deleted_file.rb

  • M modified_file.rb

  • R renamed_file_before.rb -> renamed_file_after.rb

it will return “`

'new_file.rb', 'modified_file.rb', 'renamed_file_after.rb'

“`

Returns:

  • (Array<String>)

28
29
30
31
32
33
34
35
36
# File 'lib/gitlab/danger/helper.rb', line 28

def all_changed_files
  Set.new
    .merge(git.added_files.to_a)
    .merge(git.modified_files.to_a)
    .merge(git.renamed_files.map { |x| x[:after] })
    .subtract(git.renamed_files.map { |x| x[:before] })
    .to_a
    .sort
end

#all_ee_changesObject


53
54
55
# File 'lib/gitlab/danger/helper.rb', line 53

def all_ee_changes
  all_changed_files.grep(%r{\Aee/})
end

#categories_for_file(file) ⇒ Object

Determines the categories a file is in, e.g., `[:frontend]`, `[:backend]`, or `%i[frontend engineering_productivity]` using filename regex and specific change regex if given.


100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/gitlab/danger/helper.rb', line 100

def categories_for_file(file)
  _, categories = CATEGORIES.find do |key, _|
    filename_regex, changes_regex = Array(key)

    found = filename_regex.match?(file)
    found &&= changed_lines(file).any? { |changed_line| changes_regex.match?(changed_line) } if changes_regex

    found
  end

  Array(categories || :unknown)
end

#changed_files(regex) ⇒ Object


252
253
254
# File 'lib/gitlab/danger/helper.rb', line 252

def changed_files(regex)
  all_changed_files.grep(regex)
end

#changed_lines(changed_file) ⇒ Object

Returns a string containing changed lines as git diff

Considering changing a line in lib/gitlab/usage_data.rb it will return:

[ “— a/lib/gitlab/usage_data.rb”,

"+++ b/lib/gitlab/usage_data.rb",
"+      # Test change",
"-      # Old change" ]

46
47
48
49
50
51
# File 'lib/gitlab/danger/helper.rb', line 46

def changed_lines(changed_file)
  diff = git.diff_for_file(changed_file)
  return [] unless diff

  diff.patch.split("\n").select { |line| %r{^[+-]}.match?(line) }
end

#changes_by_categoryHash<String,Array<String>>

Returns:

  • (Hash<String,Array<String>>)

90
91
92
93
94
# File 'lib/gitlab/danger/helper.rb', line 90

def changes_by_category
  all_changed_files.each_with_object(Hash.new { |h, k| h[k] = [] }) do |file, hash|
    categories_for_file(file).each { |category| hash[category] << file }
  end
end

#cherry_pick_mr?Boolean

Returns:

  • (Boolean)

223
224
225
226
227
# File 'lib/gitlab/danger/helper.rb', line 223

def cherry_pick_mr?
  return false unless gitlab_helper

  /cherry[\s-]*pick/i.match?(gitlab_helper.mr_json['title'])
end

#ee?Boolean

Returns:

  • (Boolean)

57
58
59
60
# File 'lib/gitlab/danger/helper.rb', line 57

def ee?
  # Support former project name for `dev` and support local Danger run
  %w[gitlab gitlab-ee].include?(ENV['CI_PROJECT_NAME']) || Dir.exist?(File.expand_path('../../../ee', __dir__))
end

#gitlab_helperObject


62
63
64
65
66
67
68
69
# File 'lib/gitlab/danger/helper.rb', line 62

def gitlab_helper
  # Unfortunately the following does not work:
  # - respond_to?(:gitlab)
  # - respond_to?(:gitlab, true)
  gitlab
rescue NoMethodError
  nil
end

#has_database_scoped_labels?(current_mr_labels) ⇒ Boolean

Returns:

  • (Boolean)

256
257
258
# File 'lib/gitlab/danger/helper.rb', line 256

def has_database_scoped_labels?(current_mr_labels)
  current_mr_labels.any? { |label| label.start_with?('database::') }
end

#label_for_category(category) ⇒ Object

Returns the GFM for a category label, making its best guess if it's not a category we know about.

@return


117
118
119
# File 'lib/gitlab/danger/helper.rb', line 117

def label_for_category(category)
  CATEGORY_LABELS.fetch(category, "~#{category}")
end

#labels_list(labels, sep: ', ') ⇒ Object


242
243
244
# File 'lib/gitlab/danger/helper.rb', line 242

def labels_list(labels, sep: ', ')
  labels.map { |label| %Q{~"#{label}"} }.join(sep)
end

#markdown_list(items) ⇒ Object


79
80
81
82
83
84
85
86
87
# File 'lib/gitlab/danger/helper.rb', line 79

def markdown_list(items)
  list = items.map { |item| "* `#{item}`" }.join("\n")

  if items.size > 10
    "\n<details>\n\n#{list}\n\n</details>\n"
  else
    list
  end
end

#mr_has_labels?(*labels) ⇒ Boolean

Returns:

  • (Boolean)

235
236
237
238
239
240
# File 'lib/gitlab/danger/helper.rb', line 235

def mr_has_labels?(*labels)
  return false unless gitlab_helper

  labels = labels.flatten.uniq
  (labels & gitlab_helper.mr_labels) == labels
end

#new_teammates(usernames) ⇒ Object


209
210
211
# File 'lib/gitlab/danger/helper.rb', line 209

def new_teammates(usernames)
  usernames.map { |u| Gitlab::Danger::Teammate.new('username' => u) }
end

#prepare_labels_for_mr(labels) ⇒ Object


246
247
248
249
250
# File 'lib/gitlab/danger/helper.rb', line 246

def prepare_labels_for_mr(labels)
  return '' unless labels.any?

  "/label #{labels_list(labels, sep: ' ')}"
end

#project_nameObject


75
76
77
# File 'lib/gitlab/danger/helper.rb', line 75

def project_name
  ee? ? 'gitlab' : 'gitlab-foss'
end

#release_automation?Boolean

Returns:

  • (Boolean)

71
72
73
# File 'lib/gitlab/danger/helper.rb', line 71

def release_automation?
  gitlab_helper&.mr_author == RELEASE_TOOLS_BOT
end

#sanitize_mr_title(title) ⇒ Object


213
214
215
# File 'lib/gitlab/danger/helper.rb', line 213

def sanitize_mr_title(title)
  title.gsub(DRAFT_REGEX, '').gsub(/`/, '\\\`')
end

#security_mr?Boolean

Returns:

  • (Boolean)

217
218
219
220
221
# File 'lib/gitlab/danger/helper.rb', line 217

def security_mr?
  return false unless gitlab_helper

  gitlab_helper.mr_json['web_url'].include?('/gitlab-org/security/')
end

#stable_branch?Boolean

Returns:

  • (Boolean)

229
230
231
232
233
# File 'lib/gitlab/danger/helper.rb', line 229

def stable_branch?
  return false unless gitlab_helper

  /\A\d+-\d+-stable-ee/i.match?(gitlab_helper.mr_json['target_branch'])
end