gitlab-org--gitlab-foss/tooling/lib/tooling/find_codeowners.rb

156 lines
4.0 KiB
Ruby

# frozen_string_literal: true
require 'yaml'
module Tooling
class FindCodeowners
def execute
load_definitions.each do |section, group_defintions|
puts section
group_defintions.each do |group, list|
print_entries(group, list[:entries]) if list[:entries]
print_expanded_entries(group, list) if list[:allow]
puts
end
end
end
def load_definitions
result = load_config
result.each do |section, group_defintions|
group_defintions.each do |group, definitions|
definitions.transform_values! do |rules|
case rules
when Hash
case rules[:keywords]
when Array
rules[:keywords].flat_map do |keyword|
rules[:patterns].map do |pattern|
pattern % { keyword: keyword }
end
end
else
rules[:patterns]
end
when Array
rules
end
end
end
end
result
end
def load_config
config_path = "#{__dir__}/../../config/CODEOWNERS.yml"
if YAML.respond_to?(:safe_load_file) # Ruby 3.0+
YAML.safe_load_file(config_path, symbolize_names: true)
else
YAML.safe_load(File.read(config_path), symbolize_names: true)
end
end
# Copied and modified from ee/lib/gitlab/code_owners/file.rb
def path_matches?(pattern, path)
# `FNM_DOTMATCH` makes sure we also match files starting with a `.`
# `FNM_PATHNAME` makes sure ** matches path separators
flags = ::File::FNM_DOTMATCH | ::File::FNM_PATHNAME
# BEGIN extension
flags |= ::File::FNM_EXTGLOB
# END extension
::File.fnmatch?(normalize_pattern(pattern), path, flags)
end
# Copied from ee/lib/gitlab/code_owners/file.rb
def normalize_pattern(pattern)
# Remove `\` when escaping `\#`
pattern = pattern.sub(/\A\\#/, '#')
# Replace all whitespace preceded by a \ with a regular whitespace
pattern = pattern.gsub(/\\\s+/, ' ')
return '/**/*' if pattern == '*'
unless pattern.start_with?('/')
pattern = "/**/#{pattern}"
end
if pattern.end_with?('/')
pattern = "#{pattern}**/*"
end
pattern
end
def consolidate_paths(matched_files)
matched_files.group_by(&File.method(:dirname)).flat_map do |dir, files|
# First line is the dir itself
if find_dir_maxdepth_1(dir).lines.drop(1).sort == files.sort
"#{dir}\n"
else
files
end
end.sort
end
private
def print_entries(group, entries)
entries.each do |entry|
puts "#{entry} #{group}"
end
end
def print_expanded_entries(group, list)
matched_files = git_ls_files.each_line.select do |line|
list[:allow].find do |pattern|
path = "/#{line.chomp}"
path_matches?(pattern, path) &&
(
list[:deny].nil? ||
list[:deny].none? { |pattern| path_matches?(pattern, path) }
)
end
end
consolidated = consolidate_paths(matched_files)
consolidated_again = consolidate_paths(consolidated)
# Consider the directory structure is a tree structure:
# https://en.wikipedia.org/wiki/Tree_(data_structure)
# After we consolidated the leaf entries, it could be possible that
# we can consolidate further for the new leaves. Repeat this
# process until we see no improvements.
while consolidated_again.size < consolidated.size
consolidated = consolidated_again
consolidated_again = consolidate_paths(consolidated)
end
consolidated.each do |line|
path = line.chomp
if File.directory?(path)
puts "/#{path}/ #{group}"
else
puts "/#{path} #{group}"
end
end
end
def find_dir_maxdepth_1(dir)
`find #{dir} -maxdepth 1`
end
def git_ls_files
@git_ls_files ||= `git ls-files`
end
end
end