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.
@@ -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
@@ -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 # rubocop:disable Metrics/ClassLength
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
- @codeowners_changed = false
23
- @repo = repo
24
- setup_checker
25
- if options[:interactive]
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
- @checker.fix!
42
- return unless @codeowners_changed
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
- write_codeowners
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
- if @checker.consistent?
50
- puts '✅ File is consistent'
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 setup_checker # rubocop:disable Metrics/AbcSize
62
+ def create_checker(repo)
63
+ from = options[:from]
60
64
  to = options[:to] != 'index' ? options[:to] : nil
61
- @checker = Codeowners::Checker.new(@repo, options[:from], to)
62
- @checker.when_useless_pattern = method(:suggest_fix_for)
63
- @checker.when_new_file = method(:suggest_add_to_codeowners)
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 suggest_fix_for(line)
153
- return unless options[:interactive]
154
-
155
- puts "Pattern #{line.pattern.inspect} doesn't match."
156
- suggestion = Codeowners::Cli::SuggestFileFromPattern.new(line.pattern).pick_suggestion
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