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.
- 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
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../interactive_ops'
|
4
|
+
|
5
|
+
module Codeowners
|
6
|
+
module Cli
|
7
|
+
module Wizards
|
8
|
+
# Attempt to find a name similar to one provided in the owners list.
|
9
|
+
# Suggest to add new owner to the owners list.
|
10
|
+
# Only return decision without applying any modifications.
|
11
|
+
class NewOwnerWizard
|
12
|
+
include InteractiveOps
|
13
|
+
|
14
|
+
DEFAULT_OPTIONS = {
|
15
|
+
'a' => '(a) add a new owner',
|
16
|
+
'r' => '(r) rename owner',
|
17
|
+
'i' => '(i) ignore owner in this session',
|
18
|
+
'q' => '(q) quit and save'
|
19
|
+
}.freeze
|
20
|
+
|
21
|
+
def initialize(owners_list)
|
22
|
+
@owners_list = owners_list
|
23
|
+
end
|
24
|
+
|
25
|
+
def suggest_fixing(line, new_owner)
|
26
|
+
suggested_owner = suggest_name_from_owners_list(new_owner)
|
27
|
+
case prompt(line, new_owner, suggested_owner)
|
28
|
+
when 'y' then [:rename, suggested_owner]
|
29
|
+
when 'a' then :add
|
30
|
+
when 'r' then [:rename, keep_asking_until_valid_owner]
|
31
|
+
when 'i' then :ignore
|
32
|
+
when 'q' then :quit
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def suggest_name_from_owners_list(new_owner)
|
39
|
+
require 'fuzzy_match'
|
40
|
+
search = FuzzyMatch.new(@owners_list.owners)
|
41
|
+
(suggested_owner, dice, _lev) = search.find_with_score(new_owner)
|
42
|
+
return suggested_owner if dice && dice > 0.6
|
43
|
+
end
|
44
|
+
|
45
|
+
def prompt(line, new_owner, suggested_owner)
|
46
|
+
prompt_options = build_prompt_options(suggested_owner)
|
47
|
+
ask(<<~QUESTION, limited_to: prompt_options.keys)
|
48
|
+
#{question_body(line, new_owner, suggested_owner)}
|
49
|
+
#{question_options(prompt_options)}
|
50
|
+
QUESTION
|
51
|
+
end
|
52
|
+
|
53
|
+
def question_body(line, new_owner, suggested_owner)
|
54
|
+
prompt = "Unknown owner: #{new_owner} for pattern: #{line.pattern}."
|
55
|
+
if suggested_owner
|
56
|
+
prompt + " Did you mean #{suggested_owner}?"
|
57
|
+
else
|
58
|
+
prompt + ' Choose an option:'
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def question_options(accepted_options)
|
63
|
+
accepted_options.values.join("\n")
|
64
|
+
end
|
65
|
+
|
66
|
+
def build_prompt_options(suggested_owner)
|
67
|
+
return DEFAULT_OPTIONS unless suggested_owner
|
68
|
+
|
69
|
+
{ 'y' => "(y) correct to #{suggested_owner}" }.merge(DEFAULT_OPTIONS)
|
70
|
+
end
|
71
|
+
|
72
|
+
def keep_asking_until_valid_owner
|
73
|
+
loop do
|
74
|
+
owner = ask('New owner: ')
|
75
|
+
owner = '@' + owner unless owner[0] == '@'
|
76
|
+
return owner if @owners_list.valid_owner?(owner)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../interactive_ops'
|
4
|
+
|
5
|
+
module Codeowners
|
6
|
+
module Cli
|
7
|
+
module Wizards
|
8
|
+
# Suggests to fix unrecognized lines in the codeowners file.
|
9
|
+
# Only returns decision without applying any modifications.
|
10
|
+
class UnrecognizedLineWizard
|
11
|
+
include InteractiveOps
|
12
|
+
|
13
|
+
def suggest_fixing(line)
|
14
|
+
case prompt(line)
|
15
|
+
when 'i' then :ignore
|
16
|
+
when 'y' then [:replace, keep_asking_until_valid_line]
|
17
|
+
when 'd' then :delete
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def prompt(line)
|
24
|
+
ask(<<~QUESTION, limited_to: %w[y i d])
|
25
|
+
#{line.to_s.inspect} is in unrecognized format. Would you like to edit?
|
26
|
+
(y) yes
|
27
|
+
(i) ignore
|
28
|
+
(d) delete the line
|
29
|
+
QUESTION
|
30
|
+
end
|
31
|
+
|
32
|
+
def keep_asking_until_valid_line
|
33
|
+
line = nil
|
34
|
+
loop do
|
35
|
+
new_line_string = ask('New line: ')
|
36
|
+
line = Codeowners::Checker::Group::Line.build(new_line_string)
|
37
|
+
break unless line.is_a?(Codeowners::Checker::Group::UnrecognizedLine)
|
38
|
+
end
|
39
|
+
line
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../interactive_ops'
|
4
|
+
|
5
|
+
module Codeowners
|
6
|
+
module Cli
|
7
|
+
module Wizards
|
8
|
+
# Suggests to fix useless patterns in the codeowners file.
|
9
|
+
# Only returns decision without applying any modifications.
|
10
|
+
class UselessPatternWizard
|
11
|
+
include InteractiveOps
|
12
|
+
|
13
|
+
def suggest_fixing(line)
|
14
|
+
puts "Pattern #{line.pattern.inspect} doesn't match."
|
15
|
+
suggestion = Codeowners::Cli::SuggestFileFromPattern.new(line.pattern).pick_suggestion
|
16
|
+
|
17
|
+
# TODO: Handle duplicate patterns.
|
18
|
+
if suggestion
|
19
|
+
apply_suggestion(line, suggestion)
|
20
|
+
else
|
21
|
+
pattern_fix(line)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def apply_suggestion(line, suggestion)
|
28
|
+
case make_suggestion(suggestion)
|
29
|
+
when 'i' then :ignore
|
30
|
+
when 'y' then [:replace, suggestion]
|
31
|
+
when 'e' then edit_pattern(line)
|
32
|
+
when 'd' then :delete
|
33
|
+
when 'q' then :quit
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def make_suggestion(suggestion)
|
38
|
+
ask(<<~QUESTION, limited_to: %w[y i e d q])
|
39
|
+
Replace with: #{suggestion.inspect}?
|
40
|
+
(y) yes
|
41
|
+
(i) ignore
|
42
|
+
(e) edit the pattern
|
43
|
+
(d) delete the pattern
|
44
|
+
(q) quit and save
|
45
|
+
QUESTION
|
46
|
+
end
|
47
|
+
|
48
|
+
def pattern_fix(line)
|
49
|
+
case pattern_suggest_fixing
|
50
|
+
when 'e' then edit_pattern(line)
|
51
|
+
when 'i' then :ignore
|
52
|
+
when 'd' then :delete
|
53
|
+
when 'q' then :quit
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def pattern_suggest_fixing
|
58
|
+
ask(<<~QUESTION, limited_to: %w[i e d q])
|
59
|
+
(e) edit the pattern
|
60
|
+
(d) delete the pattern
|
61
|
+
(i) ignore
|
62
|
+
(q) quit and save
|
63
|
+
QUESTION
|
64
|
+
end
|
65
|
+
|
66
|
+
def edit_pattern(line)
|
67
|
+
new_pattern = ask("Replace pattern #{line.pattern.inspect} with: ")
|
68
|
+
return :nop if new_pattern.empty?
|
69
|
+
|
70
|
+
[:replace, new_pattern]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rest-client'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module Codeowners
|
7
|
+
# Fetch teams and members from GitHub and return them in list
|
8
|
+
class GithubFetcher
|
9
|
+
class << self
|
10
|
+
GITHUB_URL = 'https://api.github.com'
|
11
|
+
|
12
|
+
# Fetch teams and members from GitHub.
|
13
|
+
# authorization_token is GitHub PAT with read:org scope
|
14
|
+
# @return <Array> with GitHub teams and individuals belonging to a given GitHub organization
|
15
|
+
def get_owners(github_org, authorization_token)
|
16
|
+
headers = get_headers(authorization_token)
|
17
|
+
base_url = GITHUB_URL + '/orgs/' + github_org
|
18
|
+
owners = []
|
19
|
+
list_entities(base_url + '/teams', headers) { |team| owners << "@#{github_org}/#{team['slug']}" }
|
20
|
+
list_entities(base_url + '/members', headers) { |member| owners << "@#{member['login']}" }
|
21
|
+
owners
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
# Helper method to get properly formatted HTTP headers
|
27
|
+
def get_headers(authorization_token)
|
28
|
+
{
|
29
|
+
Accept: 'application/vnd.github.v3+json',
|
30
|
+
Authorization: "token #{authorization_token}"
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
# Helper method that loops through all pages if GitHub returns a paged response
|
35
|
+
def list_entities(first_page, headers)
|
36
|
+
next_page = first_page
|
37
|
+
loop do
|
38
|
+
response = RestClient.get(next_page, headers)
|
39
|
+
response_json = JSON.parse(response.body)
|
40
|
+
response_json.each { |entity| yield entity }
|
41
|
+
next_page = get_next_page(response)
|
42
|
+
break unless next_page
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Helper method to parse and get URL of the next page from 'link' response header
|
47
|
+
def get_next_page(response)
|
48
|
+
return nil unless response.headers[:link]
|
49
|
+
|
50
|
+
matches = response.headers[:link].match('<([^>]+)>; rel="next"')
|
51
|
+
return matches[1] if matches
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Codeowners
|
4
|
+
# This class is responsible for print the reports
|
5
|
+
class Reporter
|
6
|
+
LABELS = {
|
7
|
+
missing_ref: 'No owner defined',
|
8
|
+
useless_pattern: 'Useless patterns',
|
9
|
+
invalid_owner: 'Invalid owner',
|
10
|
+
unrecognized_line: 'Unrecognized line'
|
11
|
+
}.freeze
|
12
|
+
|
13
|
+
class << self
|
14
|
+
def print_delimiter_line(error_type)
|
15
|
+
raise ArgumentError, "unknown error type '#{error_type}'" unless LABELS.key?(error_type)
|
16
|
+
|
17
|
+
print('-' * 30, LABELS[error_type], '-' * 30)
|
18
|
+
end
|
19
|
+
|
20
|
+
def print_error(error_type, inconsistencies, meta)
|
21
|
+
case error_type
|
22
|
+
when :invalid_owner then print("#{inconsistencies} MISSING: #{meta.join(', ')}")
|
23
|
+
else print(inconsistencies.to_s)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def print(*args)
|
28
|
+
puts(*args)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: codeowners-checker
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jônatas Davi Paganini
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2019-
|
13
|
+
date: 2019-12-23 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: fuzzy_match
|
@@ -40,6 +40,34 @@ dependencies:
|
|
40
40
|
- - "~>"
|
41
41
|
- !ruby/object:Gem::Version
|
42
42
|
version: '1.5'
|
43
|
+
- !ruby/object:Gem::Dependency
|
44
|
+
name: json
|
45
|
+
requirement: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - "~>"
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '2.1'
|
50
|
+
type: :runtime
|
51
|
+
prerelease: false
|
52
|
+
version_requirements: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - "~>"
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '2.1'
|
57
|
+
- !ruby/object:Gem::Dependency
|
58
|
+
name: rest-client
|
59
|
+
requirement: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - "~>"
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: '2.1'
|
64
|
+
type: :runtime
|
65
|
+
prerelease: false
|
66
|
+
version_requirements: !ruby/object:Gem::Requirement
|
67
|
+
requirements:
|
68
|
+
- - "~>"
|
69
|
+
- !ruby/object:Gem::Version
|
70
|
+
version: '2.1'
|
43
71
|
- !ruby/object:Gem::Dependency
|
44
72
|
name: thor
|
45
73
|
requirement: !ruby/object:Gem::Requirement
|
@@ -208,14 +236,25 @@ files:
|
|
208
236
|
- lib/codeowners/checker/group/unrecognized_line.rb
|
209
237
|
- lib/codeowners/checker/line_grouper.rb
|
210
238
|
- lib/codeowners/checker/owner.rb
|
239
|
+
- lib/codeowners/checker/owners_list.rb
|
211
240
|
- lib/codeowners/checker/version.rb
|
212
241
|
- lib/codeowners/cli/base.rb
|
213
|
-
- lib/codeowners/cli/check.rb
|
214
242
|
- lib/codeowners/cli/config.rb
|
215
243
|
- lib/codeowners/cli/filter.rb
|
244
|
+
- lib/codeowners/cli/interactive_ops.rb
|
245
|
+
- lib/codeowners/cli/interactive_resolver.rb
|
246
|
+
- lib/codeowners/cli/interactive_runner.rb
|
216
247
|
- lib/codeowners/cli/main.rb
|
248
|
+
- lib/codeowners/cli/owners_list_handler.rb
|
217
249
|
- lib/codeowners/cli/suggest_file_from_pattern.rb
|
250
|
+
- lib/codeowners/cli/wizards.rb
|
251
|
+
- lib/codeowners/cli/wizards/new_file_wizard.rb
|
252
|
+
- lib/codeowners/cli/wizards/new_owner_wizard.rb
|
253
|
+
- lib/codeowners/cli/wizards/unrecognized_line_wizard.rb
|
254
|
+
- lib/codeowners/cli/wizards/useless_pattern_wizard.rb
|
218
255
|
- lib/codeowners/config.rb
|
256
|
+
- lib/codeowners/github_fetcher.rb
|
257
|
+
- lib/codeowners/reporter.rb
|
219
258
|
homepage: https://github.com/toptal/codeowners-checker
|
220
259
|
licenses:
|
221
260
|
- MIT
|
@@ -235,8 +274,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
235
274
|
- !ruby/object:Gem::Version
|
236
275
|
version: '0'
|
237
276
|
requirements: []
|
238
|
-
|
239
|
-
rubygems_version: 2.7.6
|
277
|
+
rubygems_version: 3.0.3
|
240
278
|
signing_key:
|
241
279
|
specification_version: 4
|
242
280
|
summary: Check consistency of Github CODEOWNERS and git changes.
|