codeowners-checker 1.0.0 → 1.0.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9768a17e8e26a830efa9f9808aec4a6e2fad800b1da0e7223fa68b8ee1a4c31c
4
- data.tar.gz: 0e744a329069f56f24a1ba922c1c614eef6667816de05d6b042f2c77a883b559
3
+ metadata.gz: 94005a8b1e6eae5bf06a313f752d8adc3aa060d452487f20f22d8e1bef3dddb3
4
+ data.tar.gz: f02eab71b3bbb472c9ee3ca3d721dcecd611cd55b0a8ab5746f9606b8d6f4c4b
5
5
  SHA512:
6
- metadata.gz: 02bcb19b84e96d42aedb93253baaedc4ead716c259dfc811f76d2bc75285c5a97ce69cffd4d88f82a9b5fa2891409337158fc0ba7fbef7090a55eaf9d722f41b
7
- data.tar.gz: 2d63073791a7fa205e3aee026158fd37841217316c0a6bbbdad5c75f0ac73d9d9f3ab6858296c918cfc167654ca2d3778db4dc29822b1dbc98f16ebe21ea4c33
6
+ metadata.gz: 71ab0e69066731091f2ac51afb108a1ba67b17dc6e0d7693a4227ea9964c46d7833c9ab1e89cb822187d1f05fc70a39541aaa0a9e3d30c18bd2c2ee04d5431e8
7
+ data.tar.gz: 839ea4f56af40e7dfdc9ca452c6caddd1ea4ee0d6bc5884f5dd37f7ead3fc2a89739ac8ae01279c5e1518d95feba6df02a4209607648f5051bc5508d3d480ad2
data/README.md CHANGED
@@ -9,7 +9,6 @@ between two git revisions.
9
9
 
10
10
  ## Usage
11
11
 
12
-
13
12
  ### Configure
14
13
 
15
14
  $ codeowners-checker config owner <@owner>
@@ -114,6 +113,9 @@ the comments defining the group are deleted as well as the pattern.
114
113
 
115
114
  ![Useless pattern example](demos/useless_pattern.svg)
116
115
 
116
+ You can also use [fzf](https://github.com/junegunn/fzf) to pick better results
117
+ and interactively choose the right file.
118
+
117
119
  Invalid patterns were fixed and the group `Security` was removed when deleting the only pattern
118
120
  in the group:
119
121
  ```
@@ -3,19 +3,19 @@
3
3
  lib = File.expand_path('lib', __dir__)
4
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
5
  require 'codeowners/checker/version'
6
+ require 'codeowners/cli/suggest_file_from_pattern'
6
7
 
7
8
  Gem::Specification.new do |spec|
8
9
  spec.name = 'codeowners-checker'
9
10
  spec.version = Codeowners::Checker::VERSION
10
11
  spec.authors = ['Jônatas Davi Paganini', 'Eva Kadlecová', 'Michal Papis']
11
12
  spec.email = ['open-source@toptal.com']
13
+ spec.homepage = 'https://github.com/toptal/codeowners-checker'
12
14
 
13
15
  spec.summary = 'Check consistency of Github CODEOWNERS and git changes.'
14
16
  spec.license = 'MIT'
15
17
 
16
- spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
- f.match(%r{^(test|spec|features)/})
18
- end
18
+ spec.files = Dir['codeowners-checker.gemspec', '*.{md,txt}', 'lib/**/*.rb']
19
19
  spec.bindir = 'bin'
20
20
  spec.executables = ['codeowners-checker']
21
21
  spec.require_paths = ['lib']
@@ -31,4 +31,12 @@ Gem::Specification.new do |spec|
31
31
  spec.add_development_dependency 'rubocop', '~> 0.61.1'
32
32
  spec.add_development_dependency 'rubocop-rspec', '~> 1.30'
33
33
  spec.add_development_dependency 'simplecov', '~> 0.16.1'
34
+
35
+ unless ENV['TRAVIS']
36
+ unless Codeowners::Cli::SuggestFileFromPattern.installed_fzf?
37
+ spec.post_install_message = <<~MESSAGE
38
+ Please, install `fzf` for a better experience.
39
+ MESSAGE
40
+ end
41
+ end
34
42
  end
data/issues.md ADDED
@@ -0,0 +1,29 @@
1
+ problems
2
+
3
+ - linked lines groups serve to attach or dettach specific info into a chunk of text
4
+ - linked lines belong to a group and group belong to file
5
+
6
+
7
+ what we're trying to solve
8
+
9
+ 1. we have a file that have a tree structure without holding the tree
10
+ CodeOwners holds the flat structure
11
+
12
+ 2. we need to interpret and parse the tree structure from the file
13
+ Group holds the tree structure with LineGroupers and pure lines
14
+
15
+ 3. we need to iterate over the lines to verify the file consistency
16
+ Checker do that.
17
+
18
+ 3.1 we interact with user when find similar and duplicated lines
19
+
20
+ 4. we need to remove a line and remove a group if the line was the last pattern
21
+ in the group.
22
+ - the Line knows how to remove them selves.
23
+
24
+ 5. we need to add or change lines
25
+ - the Group knows what file it belongs and allow to insert a pattern in the
26
+ middle or in the bottom of the file
27
+
28
+
29
+
@@ -34,11 +34,8 @@ module Codeowners
34
34
  changes_to_analyze.select { |_k, v| v == 'A' }.keys
35
35
  end
36
36
 
37
- def check!
38
- {
39
- missing_ref: missing_reference,
40
- useless_pattern: useless_pattern
41
- }
37
+ def fix!
38
+ catch(:user_quit) { results }
42
39
  end
43
40
 
44
41
  def changes_for_patterns(patterns)
@@ -82,7 +79,7 @@ module Codeowners
82
79
  end
83
80
 
84
81
  def pattern_has_files(pattern)
85
- @git.ls_files(pattern).any?
82
+ @git.ls_files(pattern.gsub(%r{^/}, '')).any?
86
83
  end
87
84
 
88
85
  def defined_owner?(file)
@@ -107,17 +104,29 @@ module Codeowners
107
104
  codeowners.main_group
108
105
  end
109
106
 
107
+ def consistent?
108
+ results.values.all?(&:empty?)
109
+ end
110
+
110
111
  def commit_changes!
111
112
  @git.add(codeowners_filename)
112
113
  @git.commit('Fix pattern :robot:')
113
114
  end
114
115
 
115
- private
116
-
117
116
  def codeowners_filename
118
117
  directories = ['', '.github', 'docs', '.gitlab']
119
118
  paths = directories.map { |dir| File.join(@repo_dir, dir, 'CODEOWNERS') }
120
119
  Dir.glob(paths).first || paths.first
121
120
  end
121
+
122
+ private
123
+
124
+ def results
125
+ @results ||=
126
+ {
127
+ missing_ref: missing_reference,
128
+ useless_pattern: useless_pattern
129
+ }
130
+ end
122
131
  end
123
132
  end
@@ -20,7 +20,7 @@ module Codeowners
20
20
  end
21
21
 
22
22
  def persist!
23
- file_manager.content = to_content
23
+ file_manager.content = main_group.to_file
24
24
  end
25
25
 
26
26
  def main_group
@@ -9,12 +9,15 @@ module Codeowners
9
9
  @target_dir, = File.split(@file)
10
10
  end
11
11
 
12
+ # @return <Array> of lines chomped
12
13
  def content
13
14
  @content ||= File.readlines(@file).map(&:chomp)
14
15
  rescue Errno::ENOENT
15
16
  @content = []
16
17
  end
17
18
 
19
+ # Save content to the @file
20
+ # Creates the directory of the file if needed
18
21
  def content=(content)
19
22
  @content = content
20
23
 
@@ -25,7 +25,7 @@ module Codeowners
25
25
  if object.is_a?(Group)
26
26
  object.each(&block)
27
27
  else
28
- block.call(object)
28
+ yield(object)
29
29
  end
30
30
  end
31
31
  end
@@ -38,8 +38,14 @@ module Codeowners
38
38
  @list.flat_map(&:to_content)
39
39
  end
40
40
 
41
+ def to_file
42
+ @list.flat_map(&:to_file)
43
+ end
44
+
41
45
  # Returns an array of strings representing the structure of the group.
42
46
  # It indent internal subgroups for readability and debugging purposes.
47
+ # rubocop:disable Metrics/MethodLength
48
+ # rubocop:disable Metrics/AbcSize
43
49
  def to_tree(indentation = '')
44
50
  @list.each_with_index.flat_map do |item, index|
45
51
  if indentation.empty?
@@ -53,6 +59,8 @@ module Codeowners
53
59
  end
54
60
  end
55
61
  end
62
+ # rubocop:enable Metrics/MethodLength
63
+ # rubocop:enable Metrics/AbcSize
56
64
 
57
65
  def owner
58
66
  owners.first
@@ -67,7 +75,7 @@ module Codeowners
67
75
 
68
76
  def subgroups_owned_by(owner)
69
77
  @list.flat_map do |item|
70
- return [] unless item.is_a?(Group)
78
+ next unless item.is_a?(Group)
71
79
 
72
80
  a = []
73
81
  a << item if item.owner == owner
@@ -110,7 +118,7 @@ module Codeowners
110
118
  end
111
119
 
112
120
  def ==(other)
113
- other.kind_of?(Group) && other.list == list
121
+ other.is_a?(Group) && other.list == list
114
122
  end
115
123
 
116
124
  protected
@@ -125,17 +133,21 @@ module Codeowners
125
133
  end.compact
126
134
  end
127
135
 
136
+ # rubocop:disable Metrics/AbcSize
128
137
  def insert_at_index(line)
129
- patterns = @list.grep(Pattern)
130
- new_patterns_sorted = patterns.dup.push(line).sort
131
- new_pattern_index = new_patterns_sorted.index { |l| l.equal? line }
132
-
133
- if new_pattern_index > 0 # rubocop:disable Style/NumericPredicate
134
- new_pattern_index + 1
138
+ new_patterns_sorted = @list.grep(Pattern).dup.push(line).sort
139
+ previous_line_index = new_patterns_sorted.index { |l| l.equal? line } - 1
140
+ previous_line = new_patterns_sorted[previous_line_index]
141
+ padding = previous_line.pattern.size + previous_line.whitespace - line.pattern.size
142
+ line.whitespace = [1, padding].max
143
+
144
+ if previous_line_index >= 0
145
+ @list.index { |l| l.equal? previous_line } + 1
135
146
  else
136
147
  find_last_line_of_initial_comments
137
148
  end
138
149
  end
150
+ # rubocop:enable Metrics/AbcSize
139
151
 
140
152
  def find_last_line_of_initial_comments
141
153
  @list.each_with_index do |item, index|
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'pathname'
3
4
  module Codeowners
4
5
  class Checker
5
6
  class Group
@@ -8,7 +9,7 @@ module Codeowners
8
9
  class Line
9
10
  attr_accessor :parent
10
11
 
11
- def self.build(line, transform_line_procs: nil)
12
+ def self.build(line)
12
13
  subclasses.each do |klass|
13
14
  return klass.new(line) if klass.match?(line)
14
15
  end
@@ -31,6 +32,10 @@ module Codeowners
31
32
  to_s
32
33
  end
33
34
 
35
+ def to_file
36
+ to_s
37
+ end
38
+
34
39
  def pattern?
35
40
  is_a?(Pattern)
36
41
  end
@@ -41,7 +46,7 @@ module Codeowners
41
46
 
42
47
  def remove!
43
48
  parent&.remove(self)
44
- parent = nil
49
+ self.parent = nil
45
50
  end
46
51
 
47
52
  def ==(other)
@@ -53,25 +58,6 @@ module Codeowners
53
58
  def <=>(other)
54
59
  to_s <=> other.to_s
55
60
  end
56
-
57
- # Pick all files from parent folder of pattern.
58
- # This is used to build a list of suggestions case the pattern is not
59
- # matching.
60
- # If the pattern use "*/*" it will consider "."
61
- # If the pattern uses Static files, it tries to reach the parent.
62
- # If the pattern revers to the root folder, pick all files from the
63
- # current pattern dir.
64
- def suggest_files_for_pattern
65
- parent_folders = pattern.split('/')[0..-2]
66
- parent_folders << '*' if parent_folders[-1] != '*'
67
- files = Dir[File.join(*parent_folders)] || []
68
- files.map(&method(:normalize_path))
69
- end
70
-
71
- def normalize_path(file)
72
- Pathname.new(file)
73
- .relative_path_from(Pathname.new('.')).to_s
74
- end
75
61
  end
76
62
  end
77
63
  end
@@ -1,17 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'line'
4
+ require_relative '../owner'
4
5
 
5
6
  module Codeowners
6
7
  class Checker
7
8
  class Group
8
9
  # Defines and manages line type pattern.
10
+ # Parse the line into pattern, owners and whitespaces.
9
11
  class Pattern < Line
10
- attr_accessor :pattern, :owners
12
+ attr_accessor :owners, :whitespace
13
+ attr_reader :pattern
11
14
 
12
15
  def self.match?(line)
13
16
  _pattern, *owners = line.split(/\s+/)
14
- owners.any? && owners.all? { |owner| owner.include?('@') }
17
+ Owner.valid?(*owners)
15
18
  end
16
19
 
17
20
  def initialize(line)
@@ -23,22 +26,38 @@ module Codeowners
23
26
  owners.first
24
27
  end
25
28
 
29
+ # Parse the line counting whitespaces between pattern and owners.
26
30
  def parse(line)
27
31
  @pattern, *@owners = line.split(/\s+/)
32
+ @whitespace = line.split('@').first.count(' ') - 1
28
33
  end
29
34
 
30
35
  def match_file?(file)
31
- regex.match(file)
36
+ if !pattern.include?('/') || pattern.include?('**')
37
+ File.fnmatch(pattern.gsub(%r{^/}, ''), file, File::FNM_DOTMATCH)
38
+ else
39
+ File.fnmatch(pattern.gsub(%r{^/}, ''), file, File::FNM_PATHNAME | File::FNM_DOTMATCH)
40
+ end
32
41
  end
33
42
 
34
- def to_s
35
- [@pattern, @owners].join(' ')
43
+ def pattern=(new_pattern)
44
+ @whitespace += @pattern.size - new_pattern.size
45
+ @whitespace = 1 if @whitespace < 1
46
+
47
+ @pattern = new_pattern
36
48
  end
37
49
 
38
- private
50
+ # @return String with the pattern and owners
51
+ # Use @param preserve_whitespaces to keep the previous identation.
52
+ def to_file(preserve_whitespaces: true)
53
+ line = pattern
54
+ spaces = preserve_whitespaces ? whitespace : 0
55
+ line << ' ' * spaces
56
+ [line, *owners].join(' ')
57
+ end
39
58
 
40
- def regex
41
- Regexp.new(pattern.gsub(%r{/\*\*}, '(/[^/]+)+').gsub(/\*/, '[^/]+'))
59
+ def to_s
60
+ to_file(preserve_whitespaces: false)
42
61
  end
43
62
  end
44
63
  end
@@ -13,6 +13,9 @@ module Codeowners
13
13
  @lines = lines
14
14
  end
15
15
 
16
+ # rubocop:disable Metrics/AbcSize
17
+ # rubocop:disable Metrics/CyclomaticComplexity
18
+ # rubocop:disable Metrics/MethodLength
16
19
  def call
17
20
  lines.each_with_index do |line, index|
18
21
  case line
@@ -46,6 +49,9 @@ module Codeowners
46
49
  end
47
50
  group_buffer.first
48
51
  end
52
+ # rubocop:enable Metrics/AbcSize
53
+ # rubocop:enable Metrics/CyclomaticComplexity
54
+ # rubocop:enable Metrics/MethodLength
49
55
 
50
56
  private
51
57
 
@@ -55,7 +61,7 @@ module Codeowners
55
61
  index.positive? && lines[index - 1].is_a?(Codeowners::Checker::Group::Empty)
56
62
  end
57
63
 
58
- def new_owner?(line, index)
64
+ def new_owner?(line, index) # rubocop:disable Metrics/MethodLength
59
65
  if previous_line_empty?(index)
60
66
  offset = 2
61
67
  while (index - offset).positive?
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Codeowners
4
+ class Checker
5
+ # Owner shared methods.
6
+ module Owner
7
+ def self.valid?(*owners)
8
+ owners.any? && owners.all? { |owner| owner.include?('@') }
9
+ end
10
+ end
11
+ end
12
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Codeowners
4
4
  class Checker
5
- VERSION = '1.0.0'
5
+ VERSION = '1.0.1'
6
6
  end
7
7
  end
@@ -1,29 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'fuzzy_match'
4
-
5
3
  require_relative '../checker'
6
4
  require_relative 'base'
7
5
  require_relative 'config'
8
6
  require_relative 'filter'
7
+ require_relative 'suggest_file_from_pattern'
8
+ require_relative '../checker/owner'
9
9
 
10
10
  module Codeowners
11
11
  module Cli
12
12
  # Command Line Interface used by bin/codeowners-checker.
13
- class Main < Base
13
+ class Main < Base # rubocop:disable Metrics/ClassLength
14
14
  LABEL = { missing_ref: 'Missing references', useless_pattern: 'No files matching with the pattern' }.freeze
15
15
  option :from, default: 'origin/master'
16
16
  option :to, default: 'HEAD'
17
17
  option :interactive, default: true, type: :boolean, aliases: '-i'
18
+ option :autocommit, default: false, type: :boolean, aliases: '-c'
18
19
  desc 'check REPO', 'Checks .github/CODEOWNERS consistency'
19
20
  # for pre-commit: --from HEAD --to index
20
21
  def check(repo = '.')
21
22
  @codeowners_changed = false
22
23
  @repo = repo
23
24
  setup_checker
24
- @checker.check!
25
- write_codeowners if @codeowners_changed
26
- @checker.commit_changes! if options[:interactive] && yes?('Commit changes?')
25
+ if options[:interactive]
26
+ interactive_mode
27
+ else
28
+ report_inconsistencies
29
+ end
27
30
  end
28
31
 
29
32
  desc 'filter <by-owner>', 'List owners of changed files'
@@ -34,12 +37,31 @@ module Codeowners
34
37
 
35
38
  private
36
39
 
37
- def setup_checker
40
+ def interactive_mode
41
+ @checker.fix!
42
+ return unless @codeowners_changed
43
+
44
+ write_codeowners
45
+ @checker.commit_changes! if options[:autocommit] || yes?('Commit changes?')
46
+ end
47
+
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
+ end
58
+
59
+ def setup_checker # rubocop:disable Metrics/AbcSize
38
60
  to = options[:to] != 'index' ? options[:to] : nil
39
61
  @checker = Codeowners::Checker.new(@repo, options[:from], to)
40
62
  @checker.when_useless_pattern = method(:suggest_fix_for)
41
63
  @checker.when_new_file = method(:suggest_add_to_codeowners)
42
- @checker.transformers << method(:unrecognized_line)
64
+ @checker.transformers << method(:unrecognized_line) if options[:interactive]
43
65
  end
44
66
 
45
67
  def write_codeowners
@@ -47,15 +69,24 @@ module Codeowners
47
69
  end
48
70
 
49
71
  def suggest_add_to_codeowners(file)
50
- return unless yes?("File added: #{file.inspect}. Add owner to the 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
51
78
 
52
- owner = ask('File owner(s): ')
53
- new_line = create_new_pattern(file, owner)
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
54
87
 
55
- unless new_line.pattern?
56
- puts "#{owner.inspect} is not a valid owner name."
57
- return
58
- end
88
+ def add_to_codeowners(file)
89
+ new_line = create_new_pattern(file)
59
90
 
60
91
  subgroups = @checker.main_group.subgroups_owned_by(new_line.owner)
61
92
  add_pattern(new_line, subgroups)
@@ -63,14 +94,42 @@ module Codeowners
63
94
  @codeowners_changed = true
64
95
  end
65
96
 
66
- def create_new_pattern(file, owner)
67
- line = "#{file} #{owner}"
68
- Codeowners::Checker::Group::Line.build(line)
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
69
128
  end
70
129
 
71
130
  def add_pattern(pattern, subgroups)
72
131
  unless subgroups.empty?
73
- return if insert_pattern_into_subgroup(pattern, subgroups) == true
132
+ return if insert_pattern_into_subgroup(pattern, subgroups)
74
133
  end
75
134
 
76
135
  @checker.main_group.add(pattern) if yes?('Add to the end of the CODEOWNERS file?')
@@ -91,10 +150,10 @@ module Codeowners
91
150
  end
92
151
 
93
152
  def suggest_fix_for(line)
94
- search = FuzzyMatch.new(line.suggest_files_for_pattern)
95
- suggestion = search.find(line.pattern)
153
+ return unless options[:interactive]
96
154
 
97
155
  puts "Pattern #{line.pattern.inspect} doesn't match."
156
+ suggestion = Codeowners::Cli::SuggestFileFromPattern.new(line.pattern).pick_suggestion
98
157
 
99
158
  # TODO: Handle duplicate patterns.
100
159
  if suggestion
@@ -109,22 +168,21 @@ module Codeowners
109
168
  def apply_suggestion(line, suggestion)
110
169
  case make_suggestion(suggestion)
111
170
  when 'i' then nil
112
- when 'y'
113
- line.pattern = suggestion
114
- when 'e'
115
- pattern_change(line)
116
- when 'd'
117
- line.remove!
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
118
175
  end
119
176
  end
120
177
 
121
178
  def make_suggestion(suggestion)
122
- ask(<<~QUESTION, limited_to: %w[y i e d])
179
+ ask(<<~QUESTION, limited_to: %w[y i e d q])
123
180
  Replace with: #{suggestion.inspect}?
124
181
  (y) yes
125
182
  (i) ignore
126
183
  (e) edit the pattern
127
184
  (d) delete the pattern
185
+ (q) quit and save
128
186
  QUESTION
129
187
  end
130
188
 
@@ -133,14 +191,16 @@ module Codeowners
133
191
  when 'e' then pattern_change(line)
134
192
  when 'i' then nil
135
193
  when 'd' then line.remove!
194
+ when 'q' then throw :user_quit
136
195
  end
137
196
  end
138
197
 
139
198
  def pattern_suggest_fixing
140
- ask(<<~QUESTION, limited_to: %w[i e d])
199
+ ask(<<~QUESTION, limited_to: %w[i e d q])
141
200
  (e) edit the pattern
142
201
  (d) delete the pattern
143
202
  (i) ignore
203
+ (q) quit and save
144
204
  QUESTION
145
205
  end
146
206
 
@@ -172,12 +232,39 @@ module Codeowners
172
232
 
173
233
  def unrecognized_line_new_line
174
234
  line = nil
175
- begin
235
+ loop do
176
236
  new_line_string = ask('New line: ')
177
237
  line = Codeowners::Checker::Group::Line.build(new_line_string)
178
- end while line.is_a?(Codeowners::Checker::Group::UnrecognizedLine)
238
+ break unless line.is_a?(Codeowners::Checker::Group::UnrecognizedLine)
239
+ end
240
+ @codeowners_changed = true
179
241
  line
180
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
266
+ end
267
+ end
181
268
  end
182
269
  end
183
270
  end