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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4e2f46c087d367f8963223d632e6231db0b4b010595fba6190dc362b06846af6
4
- data.tar.gz: 48aecfde1e7f978742558e66752f05e6b452b565f04a43a0a951caf4c431d333
3
+ metadata.gz: fea710f9cf51fa064b3ac9fdd73290673959ebb8f1d3420fc10db85f38d427be
4
+ data.tar.gz: 81277d6cfdcb325372374a3ef09ef7d91526672b78c25f5b5e42bb756a150ab0
5
5
  SHA512:
6
- metadata.gz: 154979b0d6a14f51fd914bcf6a2422e83e90ecf9c2afb3dfe19c476bfe0868ea1018fa6c8e53b0ee99342e8cbc7837bef2153fe8aa9f0385ef177600fd630719
7
- data.tar.gz: 15fef3967b0859b85a6c053b1cd8dfcea829e02f2cdcc701c26906e3fea03b9e8c3823cfa63691cb4cfe7b0fb9d5b76070bd4698f38ceba86a15a5f3c12efd66
6
+ metadata.gz: 9170ec3459dcaeabb7cdd6abdde1faab7739db931f5bdb01e9a202cd6b2b794a1561a033272f6fd3c41798f8dc662ceb939e2ea17a121aca26ec8318639db875
7
+ data.tar.gz: 481e5e568f458f408e2d260538dfaba02da323f2267cca7969b662ece7ec61b163f7419f04221cac7a784c26aae59bbca48e677d394e5f66c37e218dd5f509bf
data/README.md CHANGED
@@ -17,6 +17,35 @@ between two git revisions.
17
17
 
18
18
  It will configure `@owner` as the default owner in the config file.
19
19
 
20
+
21
+ ### Fetching and validating owners
22
+
23
+ By default, [check](#check-file-consistency) command will validate owners either against the prepopulated file (OWNERS)
24
+ or against data fetched from github.
25
+ Format of OWNERS is one owner per line.
26
+
27
+ Example OWNERS:
28
+ ```
29
+ @company/backend-devs
30
+ @company/frontend-devs
31
+ @john.smith
32
+ ```
33
+ GitHub credentials are taken from the following environment variables. You might want to put them into your .bashrc or equivalent:
34
+
35
+ $ export GITHUB_TOKEN='your GitHub PAT' # your personal access token from GitHub
36
+ $ export GITHUB_ORGANIZATION='company' # name of your GitHub organization
37
+
38
+ You can generate your PAT in [Settings -> Developer settings -> Personal access tokens on GitHub](https://github.com/settings/tokens) and `read:org` scope is **required**.
39
+
40
+ If you don't want to fetch the list from GitHub every-time you run codeowners-checker, you can fetch it and store in your repository
41
+ alongside of CODEOWNERS. The following prompt will also ask for your GitHub PAT/organization in case it is not already in environment:
42
+
43
+ $ codeowners-checker fetch
44
+
45
+ You can also turn off the validation using the following
46
+
47
+ $ codeowners-checker check --no-validateowners
48
+
20
49
  ### Check file consistency
21
50
 
22
51
  To check if your CODEOWNERS file is consistent with your current project you can run
@@ -4,5 +4,4 @@
4
4
  $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
5
5
 
6
6
  require 'codeowners/cli/main'
7
-
8
7
  Codeowners::Cli::Main.start ARGV
@@ -22,6 +22,8 @@ Gem::Specification.new do |spec|
22
22
 
23
23
  spec.add_dependency 'fuzzy_match', '~> 2.1'
24
24
  spec.add_dependency 'git', '~> 1.5'
25
+ spec.add_dependency 'json', '~> 2.1'
26
+ spec.add_dependency 'rest-client', '~> 2.1'
25
27
  spec.add_dependency 'thor', '~> 0.20.3'
26
28
  spec.add_development_dependency 'bundler', '~> 1.16'
27
29
  spec.add_development_dependency 'pry', '~> 0.12.2'
@@ -6,13 +6,16 @@ require 'logger'
6
6
  require_relative 'checker/code_owners'
7
7
  require_relative 'checker/file_as_array'
8
8
  require_relative 'checker/group'
9
+ require_relative 'checker/owners_list'
9
10
 
10
11
  module Codeowners
11
12
  # Check if code owners are consistent between a git repository and the CODEOWNERS file.
12
13
  # It compares what's being changed in the PR and check if the current files and folders
13
14
  # are also being declared in the CODEOWNERS file.
15
+ # By default (:validate_owners property) it also reads OWNERS with list of all
16
+ # possible/valid owners and validates every owner in CODEOWNERS is defined in OWNERS
14
17
  class Checker
15
- attr_accessor :when_useless_pattern, :when_new_file
18
+ attr_reader :owners_list
16
19
 
17
20
  # Get repo metadata and compare with the owners
18
21
  def initialize(repo, from, to)
@@ -20,10 +23,7 @@ module Codeowners
20
23
  @repo_dir = repo
21
24
  @from = from || 'HEAD'
22
25
  @to = to
23
- end
24
-
25
- def transformers
26
- @transformers ||= []
26
+ @owners_list = OwnersList.new(@repo_dir)
27
27
  end
28
28
 
29
29
  def changes_to_analyze
@@ -35,7 +35,7 @@ module Codeowners
35
35
  end
36
36
 
37
37
  def fix!
38
- catch(:user_quit) { results }
38
+ Enumerator.new { |yielder| catch(:user_quit) { results.each { |r| yielder << r } } }
39
39
  end
40
40
 
41
41
  def changes_for_patterns(patterns)
@@ -64,18 +64,13 @@ module Codeowners
64
64
  end
65
65
 
66
66
  def useless_pattern
67
- codeowners.select do |line|
68
- next unless line.pattern?
69
-
70
- unless pattern_has_files(line.pattern)
71
- @when_useless_pattern&.call(line)
72
- true
73
- end
67
+ @useless_pattern ||= codeowners.select do |line|
68
+ line.pattern? && !pattern_has_files(line.pattern)
74
69
  end
75
70
  end
76
71
 
77
72
  def missing_reference
78
- added_files.reject(&method(:defined_owner?))
73
+ @missing_reference ||= added_files.reject(&method(:defined_owner?))
79
74
  end
80
75
 
81
76
  def pattern_has_files(pattern)
@@ -89,14 +84,12 @@ module Codeowners
89
84
  return true if line.match_file?(file)
90
85
  end
91
86
 
92
- @when_new_file&.call(file) if @when_new_file
93
87
  false
94
88
  end
95
89
 
96
90
  def codeowners
97
91
  @codeowners ||= CodeOwners.new(
98
- FileAsArray.new(codeowners_filename),
99
- transformers: transformers
92
+ FileAsArray.new(CodeOwners.filename(@repo_dir))
100
93
  )
101
94
  end
102
95
 
@@ -105,28 +98,32 @@ module Codeowners
105
98
  end
106
99
 
107
100
  def consistent?
108
- results.values.all?(&:empty?)
101
+ results.none?
109
102
  end
110
103
 
111
104
  def commit_changes!
112
- @git.add(codeowners_filename)
105
+ @git.add(File.realpath(@codeowners.filename))
106
+ @git.add(File.realpath(@owners_list.filename))
113
107
  @git.commit('Fix pattern :robot:')
114
108
  end
115
109
 
116
- def codeowners_filename
117
- directories = ['', '.github', 'docs', '.gitlab']
118
- paths = directories.map { |dir| File.join(@repo_dir, dir, 'CODEOWNERS') }
119
- Dir.glob(paths).first || paths.first
110
+ def unrecognized_line
111
+ @unrecognized_line ||= codeowners.select { |line| line.is_a?(Codeowners::Checker::Group::UnrecognizedLine) }
120
112
  end
121
113
 
122
114
  private
123
115
 
116
+ def invalid_owners
117
+ @invalid_owners ||= @owners_list.invalid_owners(codeowners)
118
+ end
119
+
124
120
  def results
125
- @results ||=
126
- {
127
- missing_ref: missing_reference,
128
- useless_pattern: useless_pattern
129
- }
121
+ @results ||= Enumerator.new do |yielder|
122
+ missing_reference.each { |ref| yielder << [:missing_ref, ref] }
123
+ useless_pattern.each { |pattern| yielder << [:useless_pattern, pattern] }
124
+ invalid_owners.each { |(owner, missing)| yielder << [:invalid_owner, owner, missing] }
125
+ unrecognized_line.each { |line| yielder << [:unrecognized_line, line] }
126
+ end
130
127
  end
131
128
  end
132
129
  end
@@ -9,18 +9,15 @@ module Codeowners
9
9
  class CodeOwners
10
10
  include Enumerable
11
11
 
12
- attr_reader :file_manager, :transform_line_procs
12
+ attr_reader :file_manager
13
13
 
14
- def initialize(file_manager, transformers: nil)
14
+ def initialize(file_manager)
15
15
  @file_manager = file_manager
16
- @transform_line_procs = [
17
- method(:build_line),
18
- *transformers
19
- ]
20
16
  end
21
17
 
22
18
  def persist!
23
19
  file_manager.content = main_group.to_file
20
+ file_manager.persist!
24
21
  end
25
22
 
26
23
  def main_group
@@ -35,16 +32,20 @@ module Codeowners
35
32
  main_group.to_content
36
33
  end
37
34
 
35
+ def self.filename(repo_dir)
36
+ directories = ['', '.github', 'docs', '.gitlab']
37
+ paths = directories.map { |dir| File.join(repo_dir, dir, 'CODEOWNERS') }
38
+ Dir.glob(paths).first || paths.first
39
+ end
40
+
41
+ def filename
42
+ @file_manager.filename
43
+ end
44
+
38
45
  private
39
46
 
40
47
  def list
41
- @list ||= begin
42
- list = @file_manager.content
43
- transform_line_procs.each do |transform_line_proc|
44
- list = list.flat_map { |line| transform_line_proc.call(line) }.compact
45
- end
46
- list
47
- end
48
+ @list ||= @file_manager.content.flat_map { |line| build_line(line) }.compact
48
49
  end
49
50
 
50
51
  def build_line(line)
@@ -4,26 +4,27 @@ module Codeowners
4
4
  class Checker
5
5
  # Convert CODEOWNERS file content to an array.
6
6
  class FileAsArray
7
- def initialize(file)
8
- @file = file
9
- @target_dir, = File.split(@file)
7
+ attr_writer :content
8
+ attr_reader :filename
9
+
10
+ def initialize(filename)
11
+ @filename = filename
12
+ @target_dir, = File.split(@filename)
10
13
  end
11
14
 
12
15
  # @return <Array> of lines chomped
13
16
  def content
14
- @content ||= File.readlines(@file).map(&:chomp)
17
+ @content ||= File.readlines(@filename).map(&:chomp)
15
18
  rescue Errno::ENOENT
16
19
  @content = []
17
20
  end
18
21
 
19
22
  # Save content to the @file
20
23
  # Creates the directory of the file if needed
21
- def content=(content)
22
- @content = content
23
-
24
+ def persist!
24
25
  Dir.mkdir(@target_dir) unless Dir.exist?(@target_dir)
25
26
 
26
- File.open(@file, 'w+') do |f|
27
+ File.open(@filename, 'w+') do |f|
27
28
  f.puts content
28
29
  end
29
30
  end
@@ -26,6 +26,11 @@ module Codeowners
26
26
  owners.first
27
27
  end
28
28
 
29
+ def rename_owner(owner, new_owner)
30
+ owners.delete(owner)
31
+ owners << new_owner unless owners.include?(new_owner)
32
+ end
33
+
29
34
  # Parse the line counting whitespaces between pattern and owners.
30
35
  def parse(line)
31
36
  @pattern, *@owners = line.split(/\s+/)
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'file_as_array'
4
+
5
+ module Codeowners
6
+ class Checker
7
+ # Manage OWNERS file reading, re-writing and fetching
8
+ class OwnersList
9
+ attr_accessor :validate_owners, :filename
10
+ attr_writer :owners
11
+
12
+ def initialize(repo)
13
+ @validate_owners = true
14
+ # doing gsub here ensures the files are always in the same directory
15
+ @filename = CodeOwners.filename(repo).gsub('CODEOWNERS', 'OWNERS')
16
+ end
17
+
18
+ def persist!
19
+ owners_file = FileAsArray.new(@filename)
20
+ owners_file.content = @owners
21
+ owners_file.persist!
22
+ end
23
+
24
+ def valid_owner?(owner)
25
+ !@validate_owners || owners.include?(owner)
26
+ end
27
+
28
+ def owners
29
+ return [] unless @validate_owners
30
+
31
+ @owners ||=
32
+ if github_credentials_exist?
33
+ Codeowners::GithubFetcher.get_owners(ENV['GITHUB_ORGANIZATION'], ENV['GITHUB_TOKEN'])
34
+ else
35
+ FileAsArray.new(@filename).content
36
+ end
37
+ end
38
+
39
+ def github_credentials_exist?
40
+ token = ENV['GITHUB_TOKEN']
41
+ organization = ENV['GITHUB_ORGANIZATION']
42
+ token && organization
43
+ end
44
+
45
+ def invalid_owners(codeowners)
46
+ return [] unless @validate_owners
47
+
48
+ codeowners.each_with_object([]) do |line, acc|
49
+ next unless line.pattern?
50
+
51
+ missing = line.owners - owners
52
+ acc.push([line, missing]) if missing.any?
53
+ end
54
+ end
55
+
56
+ def <<(owner)
57
+ return if @owners.include?(owner)
58
+
59
+ @owners << owner
60
+ end
61
+ end
62
+ end
63
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Codeowners
4
4
  class Checker
5
- VERSION = '1.0.4'
5
+ VERSION = '1.0.5'
6
6
  end
7
7
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Codeowners
4
+ module Cli
5
+ # Provides convenience methods like :ask, :yes? without subclassing Thor
6
+ module InteractiveOps
7
+ def yes?(statement, color = nil)
8
+ thor.yes?(statement, color)
9
+ end
10
+
11
+ def ask(statement, *args)
12
+ thor.ask(statement, *args)
13
+ end
14
+
15
+ private
16
+
17
+ def thor
18
+ @thor ||= Thor.new
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'wizards/new_file_wizard'
4
+ require_relative 'wizards/new_owner_wizard'
5
+ require_relative 'wizards/unrecognized_line_wizard'
6
+ require_relative 'wizards/useless_pattern_wizard'
7
+
8
+ module Codeowners
9
+ module Cli
10
+ # Resolve issues in interactive mode
11
+ # handle_* methods will throw :user_quit
12
+ # if the user chose to save and quit
13
+ class InteractiveResolver
14
+ def initialize(checker, validate_owners, default_owner)
15
+ @checker = checker
16
+ @ignored_owners = []
17
+ @validate_owners = validate_owners
18
+ @default_owner = default_owner
19
+ create_wizards
20
+ end
21
+
22
+ def handle(error_type, inconsistencies, meta)
23
+ case error_type
24
+ when :useless_pattern then handle_useless_pattern(inconsistencies)
25
+ when :missing_ref then handle_new_file(inconsistencies)
26
+ when :invalid_owner then handle_new_owners(inconsistencies, meta)
27
+ when :unrecognized_line then process_parsed_line(inconsistencies)
28
+ else raise ArgumentError, "unknown error_type: #{error_type}"
29
+ end
30
+ end
31
+
32
+ def handle_new_file(file) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
33
+ choice, pattern = @new_file_wizard.suggest_adding(file, @checker.main_group)
34
+ throw :user_quit if choice == :quit
35
+ return unless choice == :add
36
+
37
+ validate_owner(pattern, pattern.owner) if @validate_owners
38
+ op, subgroup = @new_file_wizard.select_operation(pattern, @checker.main_group)
39
+ case op
40
+ when :insert_into_subgroup
41
+ subgroup.insert(pattern)
42
+ @made_changes = true
43
+ when :add_to_main_group
44
+ @checker.main_group.add(pattern)
45
+ @made_changes = true
46
+ end
47
+ end
48
+
49
+ def handle_new_owners(line, missing_owners)
50
+ missing_owners.each { |owner| handle_new_owner(line, owner) }
51
+ end
52
+
53
+ def handle_new_owner(line, owner) # rubocop:disable Metrics/MethodLength
54
+ return if @ignored_owners.include?(owner)
55
+
56
+ choice, new_owner = @new_owner_wizard.suggest_fixing(line, owner)
57
+ case choice
58
+ when :add
59
+ @checker.owners_list << owner
60
+ @made_changes = true
61
+ when :rename
62
+ line.rename_owner(owner, new_owner)
63
+ @checker.owners_list << new_owner
64
+ @made_changes = true
65
+ when :ignore
66
+ @ignored_owners << owner
67
+ when :quit then throw :user_quit
68
+ end
69
+ end
70
+
71
+ def handle_useless_pattern(line)
72
+ choice, new_pattern = @useless_pattern_wizard.suggest_fixing(line)
73
+ case choice
74
+ when :replace
75
+ line.pattern = new_pattern
76
+ @made_changes = true
77
+ when :delete
78
+ line.remove!
79
+ @made_changes = true
80
+ when :quit then throw :user_quit
81
+ end
82
+ end
83
+
84
+ def process_parsed_line(line) # rubocop:disable Metrics/MethodLength
85
+ return line unless line.is_a?(Codeowners::Checker::Group::UnrecognizedLine)
86
+
87
+ choice, new_line = @unrecognized_line_wizard.suggest_fixing(line)
88
+ case choice
89
+ when :replace
90
+ @made_changes = true
91
+ new_line
92
+ when :delete
93
+ @made_changes = true
94
+ nil
95
+ when :ignore then line
96
+ end
97
+ end
98
+
99
+ def print_epilogue
100
+ return unless @ignored_owners.any?
101
+
102
+ puts 'Ignored owners:'
103
+ @ignored_owners.each do |owner|
104
+ puts " * #{owner}"
105
+ end
106
+ end
107
+
108
+ def made_changes?
109
+ @made_changes
110
+ end
111
+
112
+ private
113
+
114
+ def create_wizards
115
+ @new_owner_wizard = Wizards::NewOwnerWizard.new(@checker.owners_list)
116
+ @new_file_wizard = Wizards::NewFileWizard.new(@default_owner)
117
+ @useless_pattern_wizard = Wizards::UselessPatternWizard.new
118
+ @unrecognized_line_wizard = Wizards::UnrecognizedLineWizard.new
119
+ end
120
+
121
+ def validate_owner(pattern, owner)
122
+ return if @checker.owners_list.valid_owner?(pattern.owner)
123
+
124
+ handle_new_owners(pattern, [owner])
125
+ end
126
+ end
127
+ end
128
+ end