Skip to content
代码片段 群组 项目
未验证 提交 1eded357 编辑于 作者: Kerri Miller's avatar Kerri Miller 提交者: GitLab
浏览文件

Merge branch '41914-exempt-specific-files-from-codeowners-rules' into 'master'

No related branches found
No related tags found
无相关合并请求
---
name: codeowners_file_exclusions
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/41914
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/180162
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/517075
milestone: '17.9'
group: group::source code
type: gitlab_com_derisk
default_enabled: false
...@@ -5,16 +5,20 @@ module CodeOwners ...@@ -5,16 +5,20 @@ module CodeOwners
class Entry class Entry
include ::Gitlab::Utils::StrongMemoize include ::Gitlab::Utils::StrongMemoize
Data = Struct.new(:pattern, :owner_line, :section, :optional, :approvals_required) Data = Struct.new(:pattern, :owner_line, :section, :optional, :approvals_required, :exclusion)
attr_reader :data attr_reader :data
protected :data protected :data
delegate :pattern, :hash, :owner_line, :section, :approvals_required, to: :data delegate :pattern, :hash, :owner_line, :section, :approvals_required, :exclusion, to: :data
def initialize(pattern, owner_line, section = Section::DEFAULT, optional = false, approvals_required = 0) # rubocop:disable Metrics/ParameterLists -- TODO: Reduce number of parameters in this method
@data = Data.new(pattern, owner_line, section, optional, approvals_required) def initialize(
pattern, owner_line, section = Section::DEFAULT, optional = false, approvals_required = 0,
exclusion = false)
@data = Data.new(pattern, owner_line, section, optional, approvals_required, exclusion)
end end
# rubocop:enable Metrics/ParameterLists
def all_users(project) def all_users(project)
strong_memoize(:all_users) do strong_memoize(:all_users) do
...@@ -64,6 +68,10 @@ def optional? ...@@ -64,6 +68,10 @@ def optional?
data.optional data.optional
end end
def exclusion?
data.exclusion
end
def ==(other) def ==(other)
return false unless other.is_a?(self.class) return false unless other.is_a?(self.class)
......
...@@ -61,11 +61,20 @@ def entries_for_path(path) ...@@ -61,11 +61,20 @@ def entries_for_path(path)
matches = [] matches = []
parsed_data.each do |_, section_entries| parsed_data.each do |_, section_entries|
matching_pattern = section_entries.keys.reverse.detect do |pattern| if Feature.enabled?(:codeowners_file_exclusions, @blob.repository.project)
path_matches?(pattern, path) matching_patterns = section_entries.keys.reverse.select { |pattern| path_matches?(pattern, path) }
end matching_entries = matching_patterns.map { |pattern| section_entries[pattern] }
next if matching_entries.any?(&:exclusion?)
matches << matching_entries.first.dup if matching_entries.any?
else
matching_pattern = section_entries.keys.reverse.detect do |pattern|
path_matches?(pattern, path)
end
matches << section_entries[matching_pattern].dup if matching_pattern matches << section_entries[matching_pattern].dup if matching_pattern
end
end end
matches matches
...@@ -120,6 +129,12 @@ def get_parsed_data ...@@ -120,6 +129,12 @@ def get_parsed_data
def parse_entry(line, parsed, section, line_number) def parse_entry(line, parsed, section, line_number)
pattern, _separator, entry_owners = line.partition(/(?<!\\)\s+/) pattern, _separator, entry_owners = line.partition(/(?<!\\)\s+/)
if Feature.enabled?(:codeowners_file_exclusions, @blob.repository.project)
is_exclusion = pattern.start_with?('!')
pattern = pattern[1..] if is_exclusion
end
normalized_pattern = normalize_pattern(pattern) normalized_pattern = normalize_pattern(pattern)
if entry_owners.present? && ReferenceExtractor.new(entry_owners).references.blank? if entry_owners.present? && ReferenceExtractor.new(entry_owners).references.blank?
...@@ -130,12 +145,17 @@ def parse_entry(line, parsed, section, line_number) ...@@ -130,12 +145,17 @@ def parse_entry(line, parsed, section, line_number)
add_error(Error::MISSING_ENTRY_OWNER, line_number) if owners.blank? add_error(Error::MISSING_ENTRY_OWNER, line_number) if owners.blank?
parsed[section.name][normalized_pattern] = Entry.new( entry_args = [
pattern, pattern,
owners, owners,
section.name, section.name,
section.optional, section.optional,
section.approvals) section.approvals
]
entry_args << is_exclusion if Feature.enabled?(:codeowners_file_exclusions, @blob.repository.project)
parsed[section.name][normalized_pattern] = Entry.new(*entry_args)
end end
def skip?(line) def skip?(line)
......
...@@ -21,6 +21,50 @@ def owner_line(pattern) ...@@ -21,6 +21,50 @@ def owner_line(pattern)
file.parsed_data["codeowners"][pattern].owner_line file.parsed_data["codeowners"][pattern].owner_line
end end
context 'when handling exclusion patterns' do
let(:file_content) do
<<~CONTENT
* @group-x
!*.rb
[Ruby]
*.rb @ruby-devs
/config/example.yml @config-yml
!/config/**/*.rb
CONTENT
end
it 'excludes patterns correctly per section' do
parsed = file.parsed_data
expect(parsed['codeowners'].keys).to contain_exactly('/**/*', '/**/*.rb')
expect(parsed['codeowners']['/**/*'].owner_line).to eq('@group-x')
expect(parsed['codeowners']['/**/*.rb'].exclusion).to be(true)
expect(parsed['Ruby'].keys).to contain_exactly('/**/*.rb', '/config/example.yml', '/config/**/*.rb')
expect(parsed['Ruby']['/**/*.rb'].owner_line).to eq('@ruby-devs')
expect(parsed['Ruby']['/config/example.yml'].owner_line).to eq('@config-yml')
expect(parsed['Ruby']['/config/**/*.rb'].exclusion).to be(true)
end
context 'when "codeowners_file_exclusions" is disabled' do
before do
stub_feature_flags(codeowners_file_exclusions: false)
end
it 'does not mark excluded entries per section' do
parsed = file.parsed_data
expect(parsed['codeowners'].keys).to contain_exactly('/**/*', '/**/!*.rb')
expect(parsed['codeowners']['/**/*'].owner_line).to eq('@group-x')
expect(parsed['Ruby'].keys).to contain_exactly('/**/*.rb', '/config/example.yml', '/**/!/config/**/*.rb')
expect(parsed['Ruby']['/**/*.rb'].owner_line).to eq('@ruby-devs')
expect(parsed['Ruby']['/config/example.yml'].owner_line).to eq('@config-yml')
end
end
end
context "when CODEOWNERS file contains no sections" do context "when CODEOWNERS file contains no sections" do
it 'parses all the required lines' do it 'parses all the required lines' do
expected_patterns = [ expected_patterns = [
...@@ -428,6 +472,76 @@ def owner_line(pattern) ...@@ -428,6 +472,76 @@ def owner_line(pattern)
end end
end end
context 'when handling excluded patterns' do
let(:file_content) do
<<~CONTENT
* @group-x
!*.rb
[Ruby]
*.rb @ruby-devs
!/config/*
CONTENT
end
it 'matches non-excluded files to default owner' do
entry = file.entries_for_path('file.txt').first
expect(entry.owner_line).to eq('@group-x')
end
it 'matches .rb files to ruby owner except in config' do
ruby_entry = file.entries_for_path('app/models/user.rb').first
config_entry = file.entries_for_path('config/routes.rb')
expect(ruby_entry.owner_line).to eq('@ruby-devs')
expect(config_entry).to be_empty
end
it 'does not match excluded patterns' do
entries = file.entries_for_path('config/database.rb')
expect(entries).to be_empty
end
context 'with nested exclusions' do
let(:file_content) do
<<~CONTENT
* @group-x
!/app/temp/*
!/app/*/temp/*
CONTENT
end
it 'handles nested path exclusions correctly' do
regular_entry = file.entries_for_path('app/models/user.rb').first
temp_entry = file.entries_for_path('app/temp/temp.rb')
nested_temp_entry = file.entries_for_path('app/models/temp/file.rb')
expect(regular_entry.owner_line).to eq('@group-x')
expect(temp_entry).to be_empty
expect(nested_temp_entry).to be_empty
end
context 'when "codeowners_file_exclusions" is disabled' do
before do
stub_feature_flags(codeowners_file_exclusions: false)
end
it 'handles nested path exclusions correctly' do
regular_entry = file.entries_for_path('app/models/user.rb').first
temp_entry = file.entries_for_path('app/temp/temp.rb').first
nested_temp_entry = file.entries_for_path('app/models/temp/file.rb').first
expect(regular_entry.owner_line).to eq('@group-x')
expect(temp_entry.owner_line).to eq('@group-x')
expect(nested_temp_entry.owner_line).to eq('@group-x')
end
end
end
end
context "when CODEOWNERS file contains no sections" do context "when CODEOWNERS file contains no sections" do
it_behaves_like "returns expected matches" it_behaves_like "returns expected matches"
end end
......
0% 加载中 .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册