Commit 03f7e89e authored by Rémy Coutable's avatar Rémy Coutable Committed by Albert Salim

Introduce Change and Changes classes to encapsulate changes for Danger

This encapsulate logic about the changes into a dedicated class.
Signed-off-by: default avatarRémy Coutable <remy@rymai.me>
parent 20083ff6
...@@ -54,7 +54,7 @@ def check_changelog_path(path) ...@@ -54,7 +54,7 @@ def check_changelog_path(path)
warn "This MR has a Changelog file in `ee/`, but no code changes in `ee/`. Consider moving the Changelog file outside `ee/`." warn "This MR has a Changelog file in `ee/`, but no code changes in `ee/`. Consider moving the Changelog file outside `ee/`."
end end
if ee_changes.any? && changelog.ee_changelog? && changelog.required_reasons(feature_flag_helper: feature_flag).include?(:db_changes) if ee_changes.any? && changelog.ee_changelog? && changelog.required_reasons.include?(:db_changes)
warn "This MR has a Changelog file inside `ee/`, but there are database changes which [requires](https://docs.gitlab.com/ee/development/changelog.html#what-warrants-a-changelog-entry) the Changelog placement to be outside of `ee/`. Consider moving the Changelog file outside `ee/`." warn "This MR has a Changelog file inside `ee/`, but there are database changes which [requires](https://docs.gitlab.com/ee/development/changelog.html#what-warrants-a-changelog-entry) the Changelog placement to be outside of `ee/`. Consider moving the Changelog file outside `ee/`."
end end
end end
...@@ -68,8 +68,8 @@ changelog_found = changelog.found ...@@ -68,8 +68,8 @@ changelog_found = changelog.found
if changelog_found if changelog_found
check_changelog_yaml(changelog_found) check_changelog_yaml(changelog_found)
check_changelog_path(changelog_found) check_changelog_path(changelog_found)
elsif changelog.required?(feature_flag_helper: feature_flag) elsif changelog.required?
changelog.required_texts(feature_flag_helper: feature_flag).each { |_, text| fail(text) } changelog.required_texts.each { |_, text| fail(text) } # rubocop:disable Lint/UnreachableLoop
elsif changelog.optional? elsif changelog.optional?
message changelog.optional_text message changelog.optional_text
end end
This diff is collapsed.
...@@ -10,13 +10,27 @@ RSpec.describe Tooling::Danger::Helper do ...@@ -10,13 +10,27 @@ RSpec.describe Tooling::Danger::Helper do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
include DangerSpecHelper include DangerSpecHelper
let(:fake_git) { double('fake-git') }
let(:mr_author) { nil } let(:mr_author) { nil }
let(:fake_gitlab) { double('fake-gitlab', mr_author: mr_author) } let(:fake_gitlab) { double('fake-gitlab', mr_author: mr_author) }
let(:fake_danger) { new_fake_danger.include(described_class) } let(:fake_danger) { new_fake_danger.include(described_class) }
let(:added_files) { %w[added1] }
let(:modified_files) { %w[modified1] }
let(:deleted_files) { %w[deleted1] }
let(:renamed_before_file) { 'renamed_before' }
let(:renamed_after_file) { 'renamed_after' }
let(:renamed_files) { [{ before: renamed_before_file, after: renamed_after_file }] }
let(:fake_git) { double('fake-git') }
before do
allow(fake_git).to receive(:added_files) { added_files }
allow(fake_git).to receive(:modified_files) { modified_files }
allow(fake_git).to receive(:deleted_files) { deleted_files }
allow(fake_git).to receive(:renamed_files).at_least(:twice) { renamed_files }
end
subject(:helper) { fake_danger.new(git: fake_git, gitlab: fake_gitlab) } subject(:helper) { fake_danger.new(git: fake_git, gitlab: fake_gitlab) }
describe '#gitlab_helper' do describe '#gitlab_helper' do
...@@ -191,15 +205,16 @@ RSpec.describe Tooling::Danger::Helper do ...@@ -191,15 +205,16 @@ RSpec.describe Tooling::Danger::Helper do
end end
describe '#changes_by_category' do describe '#changes_by_category' do
it 'categorizes changed files' do let(:added_files) { %w[foo foo.md foo.rb foo.js] }
expect(fake_git).to receive(:added_files) { %w[foo foo.md foo.rb foo.js db/migrate/foo lib/gitlab/database/foo.rb qa/foo ee/changelogs/foo.yml] } let(:modified_files) { %w[db/migrate/foo lib/gitlab/database/foo.rb] }
allow(fake_git).to receive(:modified_files) { [] } let(:renamed_files) { [{ before: '', after: 'qa/foo' }, { before: '', after: 'ee/changelogs/foo.yml' }] }
allow(fake_git).to receive(:renamed_files) { [] }
it 'categorizes changed files' do
expect(helper.changes_by_category).to eq( expect(helper.changes_by_category).to eq(
backend: %w[foo.rb], backend: %w[foo.rb],
database: %w[db/migrate/foo lib/gitlab/database/foo.rb], database: %w[db/migrate/foo lib/gitlab/database/foo.rb],
frontend: %w[foo.js], frontend: %w[foo.js],
migration: %w[db/migrate/foo],
none: %w[ee/changelogs/foo.yml foo.md], none: %w[ee/changelogs/foo.yml foo.md],
qa: %w[qa/foo], qa: %w[qa/foo],
unknown: %w[foo] unknown: %w[foo]
...@@ -207,6 +222,62 @@ RSpec.describe Tooling::Danger::Helper do ...@@ -207,6 +222,62 @@ RSpec.describe Tooling::Danger::Helper do
end end
end end
describe 'Tooling::Danger::Helper::Changes', :aggregate_failures do
let(:added_files) { %w[db/migrate/foo ee/changelogs/unreleased/foo.yml] }
describe '#has_category?' do
it 'returns true when changes include given category, false otherwise' do
changes = helper.changes
expect(changes.has_category?(:migration)).to eq(true)
expect(changes.has_category?(:changelog)).to eq(true)
expect(changes.has_category?(:backend)).to eq(false)
end
end
describe '#by_category' do
it 'returns an array of Change objects' do
expect(helper.changes.by_category(:migration)).to all(be_an(described_class::Change))
end
it 'returns an array of Change objects with the given category' do
changes = helper.changes
expect(changes.by_category(:migration).files).to eq(['db/migrate/foo'])
expect(changes.by_category(:changelog).files).to eq(['ee/changelogs/unreleased/foo.yml'])
expect(changes.by_category(:backend)).to be_empty
end
end
describe '#categories' do
it 'returns an array of category symbols' do
expect(helper.changes.categories).to contain_exactly(:database, :migration, :changelog, :unknown)
end
end
describe '#files' do
it 'returns an array of files' do
expect(helper.changes.files).to include(*added_files)
end
end
end
describe '#changes' do
it 'returns an array of Change objects' do
expect(helper.changes).to all(be_an(described_class::Change))
end
it 'groups changes by change type' do
changes = helper.changes
expect(changes.added.files).to eq(added_files)
expect(changes.modified.files).to eq(modified_files)
expect(changes.deleted.files).to eq(deleted_files)
expect(changes.renamed_before.files).to eq([renamed_before_file])
expect(changes.renamed_after.files).to eq([renamed_after_file])
end
end
describe '#categories_for_file' do describe '#categories_for_file' do
before do before do
allow(fake_git).to receive(:diff_for_file).with('usage_data.rb') { double(:diff, patch: "+ count(User.active)") } allow(fake_git).to receive(:diff_for_file).with('usage_data.rb') { double(:diff, patch: "+ count(User.active)") }
...@@ -304,12 +375,10 @@ RSpec.describe Tooling::Danger::Helper do ...@@ -304,12 +375,10 @@ RSpec.describe Tooling::Danger::Helper do
'db/schema.rb' | [:database] 'db/schema.rb' | [:database]
'db/structure.sql' | [:database] 'db/structure.sql' | [:database]
'db/migrate/foo' | [:database] 'db/migrate/foo' | [:database, :migration]
'db/post_migrate/foo' | [:database] 'db/post_migrate/foo' | [:database, :migration]
'ee/db/migrate/foo' | [:database] 'ee/db/geo/migrate/foo' | [:database, :migration]
'ee/db/post_migrate/foo' | [:database] 'ee/db/geo/post_migrate/foo' | [:database, :migration]
'ee/db/geo/migrate/foo' | [:database]
'ee/db/geo/post_migrate/foo' | [:database]
'app/models/project_authorization.rb' | [:database] 'app/models/project_authorization.rb' | [:database]
'app/services/users/refresh_authorized_projects_service.rb' | [:database] 'app/services/users/refresh_authorized_projects_service.rb' | [:database]
'lib/gitlab/background_migration.rb' | [:database] 'lib/gitlab/background_migration.rb' | [:database]
...@@ -400,6 +469,22 @@ RSpec.describe Tooling::Danger::Helper do ...@@ -400,6 +469,22 @@ RSpec.describe Tooling::Danger::Helper do
end end
end end
describe '#mr_iid' do
it 'returns "" when `gitlab_helper` is unavailable' do
expect(helper).to receive(:gitlab_helper).and_return(nil)
expect(helper.mr_iid).to eq('')
end
it 'returns the MR IID when `gitlab_helper` is available' do
mr_iid = '1234'
expect(fake_gitlab).to receive(:mr_json)
.and_return('iid' => mr_iid)
expect(helper.mr_iid).to eq(mr_iid)
end
end
describe '#mr_title' do describe '#mr_title' do
it 'returns "" when `gitlab_helper` is unavailable' do it 'returns "" when `gitlab_helper` is unavailable' do
expect(helper).to receive(:gitlab_helper).and_return(nil) expect(helper).to receive(:gitlab_helper).and_return(nil)
...@@ -432,6 +517,22 @@ RSpec.describe Tooling::Danger::Helper do ...@@ -432,6 +517,22 @@ RSpec.describe Tooling::Danger::Helper do
end end
end end
describe '#mr_labels' do
it 'returns "" when `gitlab_helper` is unavailable' do
expect(helper).to receive(:gitlab_helper).and_return(nil)
expect(helper.mr_labels).to eq([])
end
it 'returns the MR labels when `gitlab_helper` is available' do
mr_labels = %w[foo bar baz]
expect(fake_gitlab).to receive(:mr_labels)
.and_return(mr_labels)
expect(helper.mr_labels).to eq(mr_labels)
end
end
describe '#mr_target_branch' do describe '#mr_target_branch' do
it 'returns "" when `gitlab_helper` is unavailable' do it 'returns "" when `gitlab_helper` is unavailable' do
expect(helper).to receive(:gitlab_helper).and_return(nil) expect(helper).to receive(:gitlab_helper).and_return(nil)
......
...@@ -42,15 +42,15 @@ module Tooling ...@@ -42,15 +42,15 @@ module Tooling
This merge request requires a changelog entry because it [%<reason>s](https://docs.gitlab.com/ee/development/changelog.html#what-warrants-a-changelog-entry). This merge request requires a changelog entry because it [%<reason>s](https://docs.gitlab.com/ee/development/changelog.html#what-warrants-a-changelog-entry).
MSG MSG
def required_reasons(feature_flag_helper: nil) def required_reasons
[].tap do |reasons| [].tap do |reasons|
reasons << :db_changes if git.added_files.any? { |path| path =~ %r{\Adb/(migrate|post_migrate)/} } reasons << :db_changes if helper.changes.added.has_category?(:migration)
reasons << :feature_flag_removed if feature_flag_helper&.respond_to?(:feature_flag_files) && feature_flag_helper.feature_flag_files(change_type: :deleted).any? reasons << :feature_flag_removed if helper.changes.deleted.has_category?(:feature_flag)
end end
end end
def required?(feature_flag_helper: nil) def required?
required_reasons(feature_flag_helper: feature_flag_helper).any? required_reasons.any?
end end
def optional? def optional?
...@@ -58,7 +58,7 @@ module Tooling ...@@ -58,7 +58,7 @@ module Tooling
end end
def found def found
@found ||= git.added_files.find { |path| path =~ %r{\A(ee/)?(changelogs/unreleased)(-ee)?/} } @found ||= helper.changes.added.by_category(:changelog).files.first
end end
def ee_changelog? def ee_changelog?
...@@ -67,38 +67,34 @@ module Tooling ...@@ -67,38 +67,34 @@ module Tooling
def modified_text def modified_text
CHANGELOG_MODIFIED_URL_TEXT + CHANGELOG_MODIFIED_URL_TEXT +
format(OPTIONAL_CHANGELOG_MESSAGE, mr_iid: mr_iid, mr_title: sanitized_mr_title) format(OPTIONAL_CHANGELOG_MESSAGE, mr_iid: helper.mr_iid, mr_title: sanitized_mr_title)
end end
def required_texts(feature_flag_helper: nil) def required_texts
required_reasons(feature_flag_helper: feature_flag_helper).each_with_object({}) do |required_reason, memo| required_reasons.each_with_object({}) do |required_reason, memo|
memo[required_reason] = memo[required_reason] =
CHANGELOG_MISSING_URL_TEXT + CHANGELOG_MISSING_URL_TEXT +
format(REQUIRED_CHANGELOG_MESSAGE, reason: REQUIRED_CHANGELOG_REASONS.fetch(required_reason), mr_iid: mr_iid, mr_title: sanitized_mr_title) format(REQUIRED_CHANGELOG_MESSAGE, reason: REQUIRED_CHANGELOG_REASONS.fetch(required_reason), mr_iid: helper.mr_iid, mr_title: sanitized_mr_title)
end end
end end
def optional_text def optional_text
CHANGELOG_MISSING_URL_TEXT + CHANGELOG_MISSING_URL_TEXT +
format(OPTIONAL_CHANGELOG_MESSAGE, mr_iid: mr_iid, mr_title: sanitized_mr_title) format(OPTIONAL_CHANGELOG_MESSAGE, mr_iid: helper.mr_iid, mr_title: sanitized_mr_title)
end end
private private
def mr_iid
gitlab.mr_json["iid"]
end
def sanitized_mr_title def sanitized_mr_title
TitleLinting.sanitize_mr_title(gitlab.mr_json["title"]) TitleLinting.sanitize_mr_title(helper.mr_title)
end end
def categories_need_changelog? def categories_need_changelog?
(helper.changes_by_category.keys - NO_CHANGELOG_CATEGORIES).any? (helper.changes.categories - NO_CHANGELOG_CATEGORIES).any?
end end
def without_no_changelog_label? def without_no_changelog_label?
(gitlab.mr_labels & NO_CHANGELOG_LABELS).empty? (helper.mr_labels & NO_CHANGELOG_LABELS).empty?
end end
end end
end end
......
# frozen_string_literal: true # frozen_string_literal: true
require 'delegate'
require_relative 'teammate' require_relative 'teammate'
require_relative 'title_linting' require_relative 'title_linting'
...@@ -86,13 +88,84 @@ module Tooling ...@@ -86,13 +88,84 @@ module Tooling
end end
end end
# @return [Hash<String,Array<String>>] Change = Struct.new(:file, :change_type, :category)
class Changes < ::SimpleDelegator
def added
select_by_change_type(:added)
end
def modified
select_by_change_type(:modified)
end
def deleted
select_by_change_type(:deleted)
end
def renamed_before
select_by_change_type(:renamed_before)
end
def renamed_after
select_by_change_type(:renamed_after)
end
def has_category?(category)
any? { |change| change.category == category }
end
def by_category(category)
Changes.new(select { |change| change.category == category })
end
def categories
map(&:category).uniq
end
def files
map(&:file)
end
private
def select_by_change_type(change_type)
Changes.new(select { |change| change.change_type == change_type })
end
end
# @return [Hash<Symbol,Array<String>>]
def changes_by_category def changes_by_category
all_changed_files.each_with_object(Hash.new { |h, k| h[k] = [] }) do |file, hash| all_changed_files.each_with_object(Hash.new { |h, k| h[k] = [] }) do |file, hash|
categories_for_file(file).each { |category| hash[category] << file } categories_for_file(file).each { |category| hash[category] << file }
end end
end end
# @return [Changes]
def changes
Changes.new([]).tap do |changes|
git.added_files.each do |file|
categories_for_file(file).each { |category| changes << Change.new(file, :added, category) }
end
git.modified_files.each do |file|
categories_for_file(file).each { |category| changes << Change.new(file, :modified, category) }
end
git.deleted_files.each do |file|
categories_for_file(file).each { |category| changes << Change.new(file, :deleted, category) }
end
git.renamed_files.map { |x| x[:before] }.each do |file|
categories_for_file(file).each { |category| changes << Change.new(file, :renamed_before, category) }
end
git.renamed_files.map { |x| x[:after] }.each do |file|
categories_for_file(file).each { |category| changes << Change.new(file, :renamed_after, category) }
end
end
end
# Determines the categories a file is in, e.g., `[:frontend]`, `[:backend]`, or `%i[frontend engineering_productivity]` # 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. # using filename regex and specific change regex if given.
# #
...@@ -130,6 +203,10 @@ module Tooling ...@@ -130,6 +203,10 @@ module Tooling
CATEGORIES = { CATEGORIES = {
[%r{usage_data\.rb}, %r{^(\+|-).*\s+(count|distinct_count|estimate_batch_distinct_count)\(.*\)(.*)$}] => [:database, :backend], [%r{usage_data\.rb}, %r{^(\+|-).*\s+(count|distinct_count|estimate_batch_distinct_count)\(.*\)(.*)$}] => [:database, :backend],
%r{\A(ee/)?config/feature_flags/} => :feature_flag,
%r{\A(ee/)?(changelogs/unreleased)(-ee)?/} => :changelog,
%r{\Adoc/.*(\.(md|png|gif|jpg))\z} => :docs, %r{\Adoc/.*(\.(md|png|gif|jpg))\z} => :docs,
%r{\A(CONTRIBUTING|LICENSE|MAINTENANCE|PHILOSOPHY|PROCESS|README)(\.md)?\z} => :docs, %r{\A(CONTRIBUTING|LICENSE|MAINTENANCE|PHILOSOPHY|PROCESS|README)(\.md)?\z} => :docs,
...@@ -159,6 +236,7 @@ module Tooling ...@@ -159,6 +236,7 @@ module Tooling
\.gitlab/ci/frontend\.gitlab-ci\.yml \.gitlab/ci/frontend\.gitlab-ci\.yml
)\z}x => %i[frontend engineering_productivity], )\z}x => %i[frontend engineering_productivity],
%r{\A(ee/)?db/(geo/)?(migrate|post_migrate)/} => [:database, :migration],
%r{\A(ee/)?db/(?!fixtures)[^/]+} => :database, %r{\A(ee/)?db/(?!fixtures)[^/]+} => :database,
%r{\A(ee/)?lib/gitlab/(database|background_migration|sql|github_import)(/|\.rb)} => :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(app/models/project_authorization|app/services/users/refresh_authorized_projects_service)(/|\.rb)} => :database,
...@@ -214,6 +292,12 @@ module Tooling ...@@ -214,6 +292,12 @@ module Tooling
usernames.map { |u| Tooling::Danger::Teammate.new('username' => u) } usernames.map { |u| Tooling::Danger::Teammate.new('username' => u) }
end end
def mr_iid
return '' unless gitlab_helper
gitlab_helper.mr_json['iid']
end
def mr_title def mr_title
return '' unless gitlab_helper return '' unless gitlab_helper
...@@ -226,6 +310,12 @@ module Tooling ...@@ -226,6 +310,12 @@ module Tooling
gitlab_helper.mr_json['web_url'] gitlab_helper.mr_json['web_url']
end end
def mr_labels
return [] unless gitlab_helper
gitlab_helper.mr_labels
end
def mr_target_branch def mr_target_branch
return '' unless gitlab_helper return '' unless gitlab_helper
...@@ -257,10 +347,9 @@ module Tooling ...@@ -257,10 +347,9 @@ module Tooling
end end
def mr_has_labels?(*labels) def mr_has_labels?(*labels)
return false unless gitlab_helper
labels = labels.flatten.uniq labels = labels.flatten.uniq
(labels & gitlab_helper.mr_labels) == labels
(labels & mr_labels) == labels
end end
def labels_list(labels, sep: ', ') def labels_list(labels, sep: ', ')
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment