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.
@@ -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
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-09-10 00:00:00.000000000 Z
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
- rubyforge_project:
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.
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Codeowners
4
- module Cli
5
- class Check < Base
6
- end
7
- end
8
- end