1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
|
# 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
|