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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fea710f9cf51fa064b3ac9fdd73290673959ebb8f1d3420fc10db85f38d427be
|
4
|
+
data.tar.gz: 81277d6cfdcb325372374a3ef09ef7d91526672b78c25f5b5e42bb756a150ab0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/bin/codeowners-checker
CHANGED
data/codeowners-checker.gemspec
CHANGED
@@ -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'
|
data/lib/codeowners/checker.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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(
|
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.
|
101
|
+
results.none?
|
109
102
|
end
|
110
103
|
|
111
104
|
def commit_changes!
|
112
|
-
@git.add(
|
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
|
117
|
-
|
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
|
-
|
128
|
-
|
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
|
12
|
+
attr_reader :file_manager
|
13
13
|
|
14
|
-
def initialize(file_manager
|
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 ||=
|
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
|
-
|
8
|
-
|
9
|
-
|
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(@
|
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
|
22
|
-
@content = content
|
23
|
-
|
24
|
+
def persist!
|
24
25
|
Dir.mkdir(@target_dir) unless Dir.exist?(@target_dir)
|
25
26
|
|
26
|
-
File.open(@
|
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
|
@@ -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
|