codeowners-checker 1.0.4 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +29 -0
- data/bin/codeowners-checker +0 -1
- data/codeowners-checker.gemspec +2 -0
- data/lib/codeowners/checker.rb +25 -28
- data/lib/codeowners/checker/code_owners.rb +14 -13
- data/lib/codeowners/checker/file_as_array.rb +9 -8
- data/lib/codeowners/checker/group/pattern.rb +5 -0
- data/lib/codeowners/checker/owners_list.rb +63 -0
- data/lib/codeowners/checker/version.rb +1 -1
- data/lib/codeowners/cli/interactive_ops.rb +22 -0
- data/lib/codeowners/cli/interactive_resolver.rb +128 -0
- data/lib/codeowners/cli/interactive_runner.rb +34 -0
- data/lib/codeowners/cli/main.rb +36 -227
- data/lib/codeowners/cli/owners_list_handler.rb +32 -0
- data/lib/codeowners/cli/wizards.rb +11 -0
- data/lib/codeowners/cli/wizards/new_file_wizard.rb +94 -0
- data/lib/codeowners/cli/wizards/new_owner_wizard.rb +82 -0
- data/lib/codeowners/cli/wizards/unrecognized_line_wizard.rb +44 -0
- data/lib/codeowners/cli/wizards/useless_pattern_wizard.rb +75 -0
- data/lib/codeowners/github_fetcher.rb +55 -0
- data/lib/codeowners/reporter.rb +32 -0
- metadata +43 -5
- data/lib/codeowners/cli/check.rb +0 -8
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'interactive_resolver'
|
4
|
+
require_relative 'interactive_ops'
|
5
|
+
|
6
|
+
module Codeowners
|
7
|
+
module Cli
|
8
|
+
# Interactive session to resolve codeowners list issues
|
9
|
+
class InteractiveRunner
|
10
|
+
include InteractiveOps
|
11
|
+
|
12
|
+
attr_writer :validate_owners, :default_owner, :autocommit
|
13
|
+
|
14
|
+
def run_with(checker)
|
15
|
+
resolver = InteractiveResolver.new(checker, @validate_owners, @default_owner)
|
16
|
+
checker.fix!.each do |(error_type, inconsistencies, meta)|
|
17
|
+
resolver.handle(error_type, inconsistencies, meta)
|
18
|
+
end
|
19
|
+
resolver.print_epilogue
|
20
|
+
return unless resolver.made_changes?
|
21
|
+
|
22
|
+
write_changes(checker)
|
23
|
+
checker.commit_changes! if @autocommit || yes?('Commit changes?')
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def write_changes(checker)
|
29
|
+
checker.codeowners.persist!
|
30
|
+
checker.owners_list.persist!
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/codeowners/cli/main.rb
CHANGED
@@ -1,32 +1,37 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative '../checker'
|
4
|
+
require_relative '../reporter'
|
4
5
|
require_relative 'base'
|
5
6
|
require_relative 'config'
|
6
7
|
require_relative 'filter'
|
8
|
+
require_relative 'owners_list_handler'
|
9
|
+
require_relative '../github_fetcher'
|
7
10
|
require_relative 'suggest_file_from_pattern'
|
8
11
|
require_relative '../checker/owner'
|
12
|
+
require_relative 'interactive_runner'
|
9
13
|
|
10
14
|
module Codeowners
|
11
15
|
module Cli
|
12
16
|
# Command Line Interface used by bin/codeowners-checker.
|
13
|
-
class Main < Base
|
14
|
-
LABEL = { missing_ref: 'Missing references', useless_pattern: 'No files matching with the pattern' }.freeze
|
17
|
+
class Main < Base
|
15
18
|
option :from, default: 'origin/master'
|
16
19
|
option :to, default: 'HEAD'
|
17
20
|
option :interactive, default: true, type: :boolean, aliases: '-i'
|
21
|
+
option :validateowners, default: true, type: :boolean, aliases: '-v'
|
18
22
|
option :autocommit, default: false, type: :boolean, aliases: '-c'
|
19
23
|
desc 'check REPO', 'Checks .github/CODEOWNERS consistency'
|
20
24
|
# for pre-commit: --from HEAD --to index
|
21
25
|
def check(repo = '.')
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
interactive_mode
|
27
|
-
else
|
28
|
-
report_inconsistencies
|
26
|
+
checker = create_checker(repo)
|
27
|
+
if checker.consistent?
|
28
|
+
Reporter.print '✅ File is consistent'
|
29
|
+
exit 0
|
29
30
|
end
|
31
|
+
|
32
|
+
options[:interactive] ? interactive_mode(checker) : report_inconsistencies(checker)
|
33
|
+
|
34
|
+
exit(-1)
|
30
35
|
end
|
31
36
|
|
32
37
|
desc 'filter <by-owner>', 'List owners of changed files'
|
@@ -35,234 +40,38 @@ module Codeowners
|
|
35
40
|
desc 'config', 'Checks config is consistent or configure it'
|
36
41
|
subcommand 'config', Codeowners::Cli::Config
|
37
42
|
|
43
|
+
desc 'fetch [REPO]', 'Fetches .github/OWNERS based on github organization'
|
44
|
+
subcommand 'fetch', Codeowners::Cli::OwnersListHandler
|
45
|
+
|
38
46
|
private
|
39
47
|
|
40
|
-
def interactive_mode
|
41
|
-
|
42
|
-
|
48
|
+
def interactive_mode(checker)
|
49
|
+
runner = InteractiveRunner.new
|
50
|
+
runner.validate_owners = options[:validateowners]
|
51
|
+
runner.default_owner = @config.default_owner
|
52
|
+
runner.autocommit = options[:autocommit]
|
43
53
|
|
44
|
-
|
45
|
-
@checker.commit_changes! if options[:autocommit] || yes?('Commit changes?')
|
54
|
+
runner.run_with(checker)
|
46
55
|
end
|
47
56
|
|
48
|
-
def report_inconsistencies
|
49
|
-
|
50
|
-
|
51
|
-
exit 0
|
52
|
-
else
|
53
|
-
puts "File #{@checker.codeowners_filename} is inconsistent:"
|
54
|
-
report_errors!
|
55
|
-
exit(-1)
|
56
|
-
end
|
57
|
+
def report_inconsistencies(checker)
|
58
|
+
Reporter.print "File #{checker.codeowners.filename} is inconsistent:"
|
59
|
+
report_errors!(checker)
|
57
60
|
end
|
58
61
|
|
59
|
-
def
|
62
|
+
def create_checker(repo)
|
63
|
+
from = options[:from]
|
60
64
|
to = options[:to] != 'index' ? options[:to] : nil
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
@checker.transformers << method(:unrecognized_line) if options[:interactive]
|
65
|
-
end
|
66
|
-
|
67
|
-
def write_codeowners
|
68
|
-
@checker.codeowners.persist!
|
69
|
-
end
|
70
|
-
|
71
|
-
def suggest_add_to_codeowners(file)
|
72
|
-
case add_to_codeowners_dialog(file)
|
73
|
-
when 'y' then add_to_codeowners(file)
|
74
|
-
when 'i' then nil
|
75
|
-
when 'q' then throw :user_quit
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
def add_to_codeowners_dialog(file)
|
80
|
-
ask(<<~QUESTION, limited_to: %w[y i q])
|
81
|
-
File added: #{file.inspect}. Add owner to the CODEOWNERS file?
|
82
|
-
(y) yes
|
83
|
-
(i) ignore
|
84
|
-
(q) quit and save
|
85
|
-
QUESTION
|
86
|
-
end
|
87
|
-
|
88
|
-
def add_to_codeowners(file)
|
89
|
-
new_line = create_new_pattern(file)
|
90
|
-
|
91
|
-
subgroups = @checker.main_group.subgroups_owned_by(new_line.owner)
|
92
|
-
add_pattern(new_line, subgroups)
|
93
|
-
|
94
|
-
@codeowners_changed = true
|
95
|
-
end
|
96
|
-
|
97
|
-
def create_new_pattern(file)
|
98
|
-
sorted_owners = @checker.main_group.owners.sort
|
99
|
-
list_owners(sorted_owners)
|
100
|
-
loop do
|
101
|
-
owner = new_owner(sorted_owners)
|
102
|
-
|
103
|
-
unless Codeowners::Checker::Owner.valid?(owner)
|
104
|
-
puts "#{owner.inspect} is not a valid owner name. Try again."
|
105
|
-
next
|
106
|
-
end
|
107
|
-
|
108
|
-
return Codeowners::Checker::Group::Pattern.new("#{file} #{owner}")
|
109
|
-
end
|
110
|
-
end
|
111
|
-
|
112
|
-
def list_owners(sorted_owners)
|
113
|
-
puts 'Owners:'
|
114
|
-
sorted_owners.each_with_index { |owner, i| puts "#{i + 1} - #{owner}" }
|
115
|
-
puts "Choose owner, add new one or leave empty to use #{@config.default_owner.inspect}."
|
116
|
-
end
|
117
|
-
|
118
|
-
def new_owner(sorted_owners)
|
119
|
-
owner = ask('New owner: ')
|
120
|
-
|
121
|
-
if owner.to_i.between?(1, sorted_owners.length)
|
122
|
-
sorted_owners[owner.to_i - 1]
|
123
|
-
elsif owner.empty?
|
124
|
-
@config.default_owner
|
125
|
-
else
|
126
|
-
owner
|
127
|
-
end
|
128
|
-
end
|
129
|
-
|
130
|
-
def add_pattern(pattern, subgroups)
|
131
|
-
unless subgroups.empty?
|
132
|
-
return if insert_pattern_into_subgroup(pattern, subgroups)
|
133
|
-
end
|
134
|
-
|
135
|
-
@checker.main_group.add(pattern) if yes?('Add to the end of the CODEOWNERS file?')
|
136
|
-
end
|
137
|
-
|
138
|
-
def insert_pattern_into_subgroup(pattern, subgroups)
|
139
|
-
subgroup = suggest_subgroups_for_pattern(subgroups).to_i - 1
|
140
|
-
return unless subgroup >= 0 && subgroup < subgroups.length
|
141
|
-
|
142
|
-
subgroups[subgroup].insert(pattern)
|
143
|
-
true
|
144
|
-
end
|
145
|
-
|
146
|
-
def suggest_subgroups_for_pattern(subgroups)
|
147
|
-
puts 'Possible groups to which the pattern belongs: '
|
148
|
-
subgroups.each_with_index { |group, i| puts "#{i + 1} - #{group.title}" }
|
149
|
-
ask('Choose group: ')
|
65
|
+
checker = Codeowners::Checker.new(repo, from, to)
|
66
|
+
checker.owners_list.validate_owners = options[:validateowners]
|
67
|
+
checker
|
150
68
|
end
|
151
69
|
|
152
|
-
def
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
# TODO: Handle duplicate patterns.
|
159
|
-
if suggestion
|
160
|
-
apply_suggestion(line, suggestion)
|
161
|
-
else
|
162
|
-
pattern_fix(line)
|
163
|
-
end
|
164
|
-
|
165
|
-
@codeowners_changed = true
|
166
|
-
end
|
167
|
-
|
168
|
-
def apply_suggestion(line, suggestion)
|
169
|
-
case make_suggestion(suggestion)
|
170
|
-
when 'i' then nil
|
171
|
-
when 'y' then line.pattern = suggestion
|
172
|
-
when 'e' then pattern_change(line)
|
173
|
-
when 'd' then line.remove!
|
174
|
-
when 'q' then throw :user_quit
|
175
|
-
end
|
176
|
-
end
|
177
|
-
|
178
|
-
def make_suggestion(suggestion)
|
179
|
-
ask(<<~QUESTION, limited_to: %w[y i e d q])
|
180
|
-
Replace with: #{suggestion.inspect}?
|
181
|
-
(y) yes
|
182
|
-
(i) ignore
|
183
|
-
(e) edit the pattern
|
184
|
-
(d) delete the pattern
|
185
|
-
(q) quit and save
|
186
|
-
QUESTION
|
187
|
-
end
|
188
|
-
|
189
|
-
def pattern_fix(line)
|
190
|
-
case pattern_suggest_fixing
|
191
|
-
when 'e' then pattern_change(line)
|
192
|
-
when 'i' then nil
|
193
|
-
when 'd' then line.remove!
|
194
|
-
when 'q' then throw :user_quit
|
195
|
-
end
|
196
|
-
end
|
197
|
-
|
198
|
-
def pattern_suggest_fixing
|
199
|
-
ask(<<~QUESTION, limited_to: %w[i e d q])
|
200
|
-
(e) edit the pattern
|
201
|
-
(d) delete the pattern
|
202
|
-
(i) ignore
|
203
|
-
(q) quit and save
|
204
|
-
QUESTION
|
205
|
-
end
|
206
|
-
|
207
|
-
def pattern_change(line)
|
208
|
-
new_pattern = ask("Replace pattern #{line.pattern.inspect} with: ")
|
209
|
-
return if new_pattern.empty?
|
210
|
-
|
211
|
-
line.pattern = new_pattern
|
212
|
-
end
|
213
|
-
|
214
|
-
def unrecognized_line(line)
|
215
|
-
return line unless line.is_a?(Codeowners::Checker::Group::UnrecognizedLine)
|
216
|
-
|
217
|
-
case unrecognized_line_suggest_fixing(line)
|
218
|
-
when 'i' then line
|
219
|
-
when 'y' then unrecognized_line_new_line
|
220
|
-
when 'd' then nil
|
221
|
-
end
|
222
|
-
end
|
223
|
-
|
224
|
-
def unrecognized_line_suggest_fixing(line)
|
225
|
-
ask(<<~QUESTION, limited_to: %w[y i d])
|
226
|
-
#{line.to_s.inspect} is in unrecognized format. Would you like to edit?
|
227
|
-
(y) yes
|
228
|
-
(i) ignore
|
229
|
-
(d) delete the line
|
230
|
-
QUESTION
|
231
|
-
end
|
232
|
-
|
233
|
-
def unrecognized_line_new_line
|
234
|
-
line = nil
|
235
|
-
loop do
|
236
|
-
new_line_string = ask('New line: ')
|
237
|
-
line = Codeowners::Checker::Group::Line.build(new_line_string)
|
238
|
-
break unless line.is_a?(Codeowners::Checker::Group::UnrecognizedLine)
|
239
|
-
end
|
240
|
-
@codeowners_changed = true
|
241
|
-
line
|
242
|
-
end
|
243
|
-
|
244
|
-
def ask(message, *opts)
|
245
|
-
return unless options[:interactive]
|
246
|
-
|
247
|
-
super
|
248
|
-
end
|
249
|
-
|
250
|
-
def yes?(message, *opts)
|
251
|
-
return unless options[:interactive]
|
252
|
-
|
253
|
-
super
|
254
|
-
end
|
255
|
-
|
256
|
-
LABELS = {
|
257
|
-
missing_ref: 'No owner defined',
|
258
|
-
useless_pattern: 'Useless patterns'
|
259
|
-
}.freeze
|
260
|
-
|
261
|
-
def report_errors!
|
262
|
-
@checker.fix!.each do |error_type, inconsistencies|
|
263
|
-
next if inconsistencies.empty?
|
264
|
-
|
265
|
-
puts LABELS[error_type], '-' * 30, inconsistencies, '-' * 30
|
70
|
+
def report_errors!(checker)
|
71
|
+
checker.fix!.reduce(nil) do |prev_error_type, (error_type, inconsistencies, meta)|
|
72
|
+
Reporter.print_delimiter_line(error_type) if prev_error_type != error_type
|
73
|
+
Reporter.print_error(error_type, inconsistencies, meta)
|
74
|
+
error_type
|
266
75
|
end
|
267
76
|
end
|
268
77
|
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../checker/owners_list'
|
4
|
+
|
5
|
+
module Codeowners
|
6
|
+
module Cli
|
7
|
+
# Command Line Interface dealing with OWNERS generation and validation
|
8
|
+
class OwnersListHandler < Base
|
9
|
+
default_task :fetch
|
10
|
+
|
11
|
+
desc 'fetch [REPO]', 'Fetches .github/OWNERS based on github organization'
|
12
|
+
def fetch(repo = '.')
|
13
|
+
@repo = repo
|
14
|
+
owners = owners_from_github
|
15
|
+
owners_list = Checker::OwnersList.new(repo)
|
16
|
+
owners_list.owners = owners
|
17
|
+
owners_list.persist!
|
18
|
+
end
|
19
|
+
|
20
|
+
no_commands do
|
21
|
+
def owners_from_github
|
22
|
+
organization = ENV['GITHUB_ORGANIZATION']
|
23
|
+
organization ||= ask('GitHub organization (e.g. github): ')
|
24
|
+
token = ENV['GITHUB_TOKEN']
|
25
|
+
token ||= ask('Enter GitHub token: ', echo: false)
|
26
|
+
puts 'Fetching owners list from GitHub ...'
|
27
|
+
Codeowners::GithubFetcher.get_owners(organization, token)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Codeowners
|
4
|
+
module Cli
|
5
|
+
# Contains helper classes that prompt the user for information
|
6
|
+
# required to resolve detected issues in interactive mode.
|
7
|
+
# They only return acquired information without applying any modifications.
|
8
|
+
module Wizards
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../interactive_ops'
|
4
|
+
|
5
|
+
module Codeowners
|
6
|
+
module Cli
|
7
|
+
module Wizards
|
8
|
+
# Suggests to add new files to the codeowners list.
|
9
|
+
# Only returns decision without applying any modifications.
|
10
|
+
class NewFileWizard
|
11
|
+
include InteractiveOps
|
12
|
+
|
13
|
+
def initialize(default_owner)
|
14
|
+
@default_owner = default_owner
|
15
|
+
end
|
16
|
+
|
17
|
+
def suggest_adding(file, main_group)
|
18
|
+
case prompt(file)
|
19
|
+
when 'y' then [:add, create_pattern(file, main_group)]
|
20
|
+
when 'i' then :ignore
|
21
|
+
when 'q' then :quit
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def select_operation(pattern, main_group)
|
26
|
+
subgroups = main_group.subgroups_owned_by(pattern.owner)
|
27
|
+
if subgroups.any? && (subgroup = prompt_subgroup(subgroups))
|
28
|
+
[:insert_into_subgroup, subgroup]
|
29
|
+
elsif yes?('Add to the end of the CODEOWNERS file?')
|
30
|
+
:add_to_main_group
|
31
|
+
else
|
32
|
+
:ignore
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def prompt(file)
|
39
|
+
ask(<<~QUESTION, limited_to: %w[y i q])
|
40
|
+
File added: #{file.inspect}. Add owner to the CODEOWNERS file?
|
41
|
+
(y) yes
|
42
|
+
(i) ignore
|
43
|
+
(q) quit and save
|
44
|
+
QUESTION
|
45
|
+
end
|
46
|
+
|
47
|
+
def create_pattern(file, main_group)
|
48
|
+
sorted_owners = main_group.owners.sort
|
49
|
+
owner = prompt_owner(sorted_owners)
|
50
|
+
Codeowners::Checker::Group::Pattern.new("#{file} #{owner}")
|
51
|
+
end
|
52
|
+
|
53
|
+
def prompt_owner(sorted_owners)
|
54
|
+
list_existing_owners(sorted_owners)
|
55
|
+
loop do
|
56
|
+
owner = do_prompt_owner(sorted_owners)
|
57
|
+
|
58
|
+
unless Codeowners::Checker::Owner.valid?(owner)
|
59
|
+
puts "#{owner.inspect} is not a valid owner name. Try again."
|
60
|
+
next
|
61
|
+
end
|
62
|
+
|
63
|
+
return owner
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def list_existing_owners(sorted_owners)
|
68
|
+
puts 'Owners:'
|
69
|
+
sorted_owners.each_with_index { |owner, i| puts "#{i + 1} - #{owner}" }
|
70
|
+
puts "Choose owner, add new one or leave empty to use #{@default_owner.inspect}."
|
71
|
+
end
|
72
|
+
|
73
|
+
def do_prompt_owner(sorted_owners)
|
74
|
+
input = ask('New owner: ')
|
75
|
+
|
76
|
+
if input.to_i.between?(1, sorted_owners.length)
|
77
|
+
sorted_owners[input.to_i - 1]
|
78
|
+
elsif input.empty?
|
79
|
+
@default_owner
|
80
|
+
else
|
81
|
+
input
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def prompt_subgroup(subgroups)
|
86
|
+
puts 'Possible groups to which the pattern belongs: '
|
87
|
+
subgroups.each_with_index { |group, i| puts "#{i + 1} - #{group.title}" }
|
88
|
+
choice = ask('Choose group: ').to_i
|
89
|
+
subgroups[choice - 1] if choice.between?(1, subgroups.length)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|