codeowners-checker 1.0.4 → 1.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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