codeowners-checker 1.0.0
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 +7 -0
- data/.gitignore +12 -0
- data/.projections.json +4 -0
- data/.rspec +3 -0
- data/.rubocop.yml +28 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +78 -0
- data/Guardfile +39 -0
- data/LICENSE.txt +21 -0
- data/README.md +171 -0
- data/Rakefile +10 -0
- data/bin/codeowners-checker +8 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/codeowners-checker.gemspec +34 -0
- data/demos/missing_reference.svg +1 -0
- data/demos/useless_pattern.svg +1 -0
- data/lib/codeowners/checker.rb +123 -0
- data/lib/codeowners/checker/array.rb +15 -0
- data/lib/codeowners/checker/code_owners.rb +54 -0
- data/lib/codeowners/checker/file_as_array.rb +29 -0
- data/lib/codeowners/checker/group.rb +148 -0
- data/lib/codeowners/checker/group/comment.rb +32 -0
- data/lib/codeowners/checker/group/empty.rb +16 -0
- data/lib/codeowners/checker/group/group_begin_comment.rb +17 -0
- data/lib/codeowners/checker/group/group_end_comment.rb +17 -0
- data/lib/codeowners/checker/group/line.rb +83 -0
- data/lib/codeowners/checker/group/pattern.rb +46 -0
- data/lib/codeowners/checker/group/unrecognized_line.rb +13 -0
- data/lib/codeowners/checker/line_grouper.rb +106 -0
- data/lib/codeowners/checker/version.rb +7 -0
- data/lib/codeowners/cli/base.rb +34 -0
- data/lib/codeowners/cli/check.rb +8 -0
- data/lib/codeowners/cli/config.rb +24 -0
- data/lib/codeowners/cli/filter.rb +63 -0
- data/lib/codeowners/cli/main.rb +183 -0
- data/lib/codeowners/config.rb +48 -0
- metadata +239 -0
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'line'
|
4
|
+
|
5
|
+
module Codeowners
|
6
|
+
class Checker
|
7
|
+
class Group
|
8
|
+
# Define and manage comment line.
|
9
|
+
class Comment < Line
|
10
|
+
# Matches if the line is a comment.
|
11
|
+
# @return [Boolean] if the line start with `#`
|
12
|
+
def self.match?(line)
|
13
|
+
line.start_with?('#')
|
14
|
+
end
|
15
|
+
|
16
|
+
# Return the comment level if the comment works like a markdown
|
17
|
+
# headers.
|
18
|
+
# @return [Integer] with the heading level.
|
19
|
+
#
|
20
|
+
# @example
|
21
|
+
# Comment.new('# First level').level # => 1
|
22
|
+
# Comment.new('## Second').level # => 2
|
23
|
+
def level
|
24
|
+
(@line[/^#+/] || '').size
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
require_relative 'group_begin_comment'
|
32
|
+
require_relative 'group_end_comment'
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'comment'
|
4
|
+
|
5
|
+
module Codeowners
|
6
|
+
class Checker
|
7
|
+
class Group
|
8
|
+
# Define line type GroupBeginComment which is used for defining the beggining
|
9
|
+
# of a group.
|
10
|
+
class GroupBeginComment < Comment
|
11
|
+
def self.match?(line)
|
12
|
+
line.lstrip.start_with?(/^#+ BEGIN/)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'comment'
|
4
|
+
|
5
|
+
module Codeowners
|
6
|
+
class Checker
|
7
|
+
class Group
|
8
|
+
# Define line type GroupEndComment which is used for defining the end
|
9
|
+
# of a group.
|
10
|
+
class GroupEndComment < Comment
|
11
|
+
def self.match?(line)
|
12
|
+
line.lstrip.start_with?(/^#+ END/)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Codeowners
|
4
|
+
class Checker
|
5
|
+
class Group
|
6
|
+
# It sorts lines from CODEOWNERS file to different line types and holds
|
7
|
+
# shared methods for all lines.
|
8
|
+
class Line
|
9
|
+
attr_accessor :parent
|
10
|
+
|
11
|
+
def self.build(line, transform_line_procs: nil)
|
12
|
+
subclasses.each do |klass|
|
13
|
+
return klass.new(line) if klass.match?(line)
|
14
|
+
end
|
15
|
+
UnrecognizedLine.new(line)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.subclasses
|
19
|
+
[Empty, GroupBeginComment, GroupEndComment, Comment, Pattern]
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(line)
|
23
|
+
@line = line
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_s
|
27
|
+
@line
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_content
|
31
|
+
to_s
|
32
|
+
end
|
33
|
+
|
34
|
+
def pattern?
|
35
|
+
is_a?(Pattern)
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_tree(indentation)
|
39
|
+
indentation + to_s
|
40
|
+
end
|
41
|
+
|
42
|
+
def remove!
|
43
|
+
parent&.remove(self)
|
44
|
+
parent = nil
|
45
|
+
end
|
46
|
+
|
47
|
+
def ==(other)
|
48
|
+
return false unless other.is_a?(self.class)
|
49
|
+
|
50
|
+
other.to_s == to_s
|
51
|
+
end
|
52
|
+
|
53
|
+
def <=>(other)
|
54
|
+
to_s <=> other.to_s
|
55
|
+
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
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
require_relative 'empty'
|
81
|
+
require_relative 'comment'
|
82
|
+
require_relative 'pattern'
|
83
|
+
require_relative 'unrecognized_line'
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'line'
|
4
|
+
|
5
|
+
module Codeowners
|
6
|
+
class Checker
|
7
|
+
class Group
|
8
|
+
# Defines and manages line type pattern.
|
9
|
+
class Pattern < Line
|
10
|
+
attr_accessor :pattern, :owners
|
11
|
+
|
12
|
+
def self.match?(line)
|
13
|
+
_pattern, *owners = line.split(/\s+/)
|
14
|
+
owners.any? && owners.all? { |owner| owner.include?('@') }
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(line)
|
18
|
+
super
|
19
|
+
parse(line)
|
20
|
+
end
|
21
|
+
|
22
|
+
def owner
|
23
|
+
owners.first
|
24
|
+
end
|
25
|
+
|
26
|
+
def parse(line)
|
27
|
+
@pattern, *@owners = line.split(/\s+/)
|
28
|
+
end
|
29
|
+
|
30
|
+
def match_file?(file)
|
31
|
+
regex.match(file)
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_s
|
35
|
+
[@pattern, @owners].join(' ')
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def regex
|
41
|
+
Regexp.new(pattern.gsub(%r{/\*\*}, '(/[^/]+)+').gsub(/\*/, '[^/]+'))
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Codeowners
|
4
|
+
class Checker
|
5
|
+
# Create groups and subgroups structure for the lines in the CODEOWNERS file.
|
6
|
+
class LineGrouper
|
7
|
+
def self.call(group, lines)
|
8
|
+
new(group, lines).call
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(group, lines)
|
12
|
+
@group_buffer = [group]
|
13
|
+
@lines = lines
|
14
|
+
end
|
15
|
+
|
16
|
+
def call
|
17
|
+
lines.each_with_index do |line, index|
|
18
|
+
case line
|
19
|
+
when Codeowners::Checker::Group::Empty
|
20
|
+
ensure_groups_structure
|
21
|
+
when Codeowners::Checker::Group::GroupBeginComment
|
22
|
+
trim_groups(line.level)
|
23
|
+
create_groups_structure(line.level)
|
24
|
+
when Codeowners::Checker::Group::GroupEndComment
|
25
|
+
trim_subgroups(line.level)
|
26
|
+
create_groups_structure(line.level)
|
27
|
+
when Codeowners::Checker::Group::Comment
|
28
|
+
if previous_line_empty?(index)
|
29
|
+
trim_groups(line.level)
|
30
|
+
else
|
31
|
+
trim_subgroups(line.level)
|
32
|
+
end
|
33
|
+
create_groups_structure(line.level)
|
34
|
+
when Codeowners::Checker::Group::Pattern
|
35
|
+
if new_owner?(line, index)
|
36
|
+
trim_groups(current_level)
|
37
|
+
new_group
|
38
|
+
end
|
39
|
+
ensure_groups_structure
|
40
|
+
when Codeowners::Checker::Group::UnrecognizedLine
|
41
|
+
ensure_groups_structure
|
42
|
+
else
|
43
|
+
raise "Do not know how to handle line: #{line.inspect}"
|
44
|
+
end
|
45
|
+
current_group.add(line)
|
46
|
+
end
|
47
|
+
group_buffer.first
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
attr_reader :group_buffer, :lines
|
53
|
+
|
54
|
+
def previous_line_empty?(index)
|
55
|
+
index.positive? && lines[index - 1].is_a?(Codeowners::Checker::Group::Empty)
|
56
|
+
end
|
57
|
+
|
58
|
+
def new_owner?(line, index)
|
59
|
+
if previous_line_empty?(index)
|
60
|
+
offset = 2
|
61
|
+
while (index - offset).positive?
|
62
|
+
case lines[index - offset]
|
63
|
+
when Codeowners::Checker::Group::GroupEndComment
|
64
|
+
nil
|
65
|
+
when Codeowners::Checker::Group::Comment
|
66
|
+
return false
|
67
|
+
when Codeowners::Checker::Group::Pattern
|
68
|
+
return line.owner != lines[index - offset].owner
|
69
|
+
end
|
70
|
+
offset += 1
|
71
|
+
end
|
72
|
+
end
|
73
|
+
false
|
74
|
+
end
|
75
|
+
|
76
|
+
def current_group
|
77
|
+
group_buffer.last
|
78
|
+
end
|
79
|
+
|
80
|
+
def current_level
|
81
|
+
group_buffer.length - 1
|
82
|
+
end
|
83
|
+
|
84
|
+
def new_group
|
85
|
+
group = current_group.create_subgroup
|
86
|
+
group_buffer << group
|
87
|
+
end
|
88
|
+
|
89
|
+
def ensure_groups_structure
|
90
|
+
new_group if current_level.zero?
|
91
|
+
end
|
92
|
+
|
93
|
+
def create_groups_structure(level)
|
94
|
+
new_group while current_level < level
|
95
|
+
end
|
96
|
+
|
97
|
+
def trim_groups(level)
|
98
|
+
group_buffer.slice!(level..-1)
|
99
|
+
end
|
100
|
+
|
101
|
+
def trim_subgroups(level)
|
102
|
+
trim_groups(level + 1)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'thor'
|
4
|
+
|
5
|
+
require_relative '../config'
|
6
|
+
|
7
|
+
module Codeowners
|
8
|
+
module Cli
|
9
|
+
# Base collects shared methods used by all CLI sub commands
|
10
|
+
# It loads and validate the default config file or output an explanation
|
11
|
+
# about how to configure it.
|
12
|
+
class Base < Thor
|
13
|
+
def initialize(args = [], options = {}, config = {})
|
14
|
+
super
|
15
|
+
@config ||= config[:config] || default_config
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
attr_reader :config
|
21
|
+
|
22
|
+
def default_config
|
23
|
+
Codeowners::Config.new
|
24
|
+
end
|
25
|
+
|
26
|
+
def help_stderr
|
27
|
+
save_stdout = $stdout
|
28
|
+
$stdout = $stderr
|
29
|
+
help
|
30
|
+
$stdout = save_stdout
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
|
5
|
+
module Codeowners
|
6
|
+
module Cli
|
7
|
+
# Provide options for configuring the default owner used for filtering changes.
|
8
|
+
class Config < Base
|
9
|
+
default_task :list
|
10
|
+
|
11
|
+
desc 'list', 'List the default owner configured in the config file'
|
12
|
+
def list
|
13
|
+
puts(config.to_h.map { |name, value| "#{name}: #{value.inspect}" })
|
14
|
+
help_stderr if config.default_owner.empty?
|
15
|
+
end
|
16
|
+
|
17
|
+
desc 'owner <name>', 'Configure a default owner name'
|
18
|
+
def owner(name)
|
19
|
+
config.default_owner = name
|
20
|
+
puts "Default owner configured to #{name}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Codeowners
|
4
|
+
module Cli
|
5
|
+
# List changed files. Provide an option to list all the changed files grouped by
|
6
|
+
# the owner of the file or filter them and show only the files owned by default owner.
|
7
|
+
class Filter < Base
|
8
|
+
option :from, default: 'origin/master'
|
9
|
+
option :to, default: 'HEAD'
|
10
|
+
option :verbose, default: false, type: :boolean, aliases: '-v'
|
11
|
+
desc 'by <owner>', <<~DESC
|
12
|
+
Lists changed files owned by owner.
|
13
|
+
If no owner is specified, default owner is taken from the config file.
|
14
|
+
DESC
|
15
|
+
# option :local, default: false, type: :boolean, aliases: '-l'
|
16
|
+
# option :branch, default: '', aliases: '-b'
|
17
|
+
def by(owner = config.default_owner)
|
18
|
+
return if owner.empty?
|
19
|
+
|
20
|
+
changes = checker.changes_with_ownership(owner)
|
21
|
+
if changes.key?(owner)
|
22
|
+
changes.values.each { |file| puts file }
|
23
|
+
else
|
24
|
+
puts "Owner #{owner} not defined in .github/CODEOWNERS"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
option :from, default: 'origin/master'
|
29
|
+
option :to, default: 'HEAD'
|
30
|
+
option :verbose, default: false, type: :boolean, aliases: '-v'
|
31
|
+
desc 'all', 'Lists all changed files grouped by owner'
|
32
|
+
def all
|
33
|
+
changes = checker.changes_with_ownership.select { |_owner, val| val && !val.empty? }
|
34
|
+
changes.keys.each do |owner|
|
35
|
+
puts(owner + ":\n " + changes[owner].join("\n ") + "\n\n")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def initialize(args = [], options = {}, config = {})
|
40
|
+
super
|
41
|
+
@repo_base_path = `git rev-parse --show-toplevel`
|
42
|
+
if !@repo_base_path || @repo_base_path.empty?
|
43
|
+
raise 'You must be positioned in a git repository to use this tool'
|
44
|
+
end
|
45
|
+
|
46
|
+
@repo_base_path.chomp!
|
47
|
+
Dir.chdir(@repo_base_path)
|
48
|
+
|
49
|
+
@checker ||= config[:checker] || default_checker
|
50
|
+
end
|
51
|
+
|
52
|
+
default_task :by
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
attr_reader :checker
|
57
|
+
|
58
|
+
def default_checker
|
59
|
+
Codeowners::Checker.new(@repo_base_path, options[:from], options[:to])
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|