code_owners 1.0.9 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.github/CODEOWNERS +1 -0
- data/Gemfile.lock +3 -1
- data/README.md +17 -4
- data/bin/code_owners +23 -12
- data/code_owners.gemspec +1 -0
- data/lib/code_owners/version.rb +1 -1
- data/lib/code_owners.rb +143 -47
- data/spec/code_owners_spec.rb +173 -42
- data/spec/files/.dot folder/.silly example.txt +1 -0
- data/spec/files/.dot folder/afoodle.txt +1 -0
- data/spec/files/.dot folder/serious_example.txt +1 -0
- data/spec/files/.gitignore +1 -0
- data/spec/files/foo/bar/bar +1 -0
- data/spec/files/foo/bar/bar.txt +1 -0
- data/spec/files/foo/bar/baz/baz.txt +1 -0
- data/spec/files/foo/bar/baz/baz2.txt +1 -0
- data/spec/files/foo/bar/some bar.txt +1 -0
- data/spec/files/foo/confoozing.txt +1 -0
- data/spec/files/foo/fake_gem.gem +1 -0
- data/spec/files/foo/foo +1 -0
- data/spec/files/foo/foo.txt +1 -0
- data/spec/files/foo/some foo.text +1 -0
- data/spec/generate_permutations.rb +74 -0
- data/spec/permutations.json +2920 -0
- metadata +32 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: baea109754326800382bc8665df1e20634680187a1209f612bcc14f1447a962e
|
4
|
+
data.tar.gz: 25975f589029d740031c6c14832341a605fa885386be3f88bcb992cb427b1bf0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: be4b52ceca387c455f3b597f47c3f61aed0d3c381eb4d52e6f2d9c25311d0505704ebb817af0ab73892521bdb1f804fc3a560bfb49c54e1f62b03bf20fe6b796
|
7
|
+
data.tar.gz: b31b2929d9023a3609bbd8286b4579ff87c8af5f4a6fc9025ccce9f8bdbddb65c09facf92c7a6873dd20423e925e083de41ba7de8800210ea2cf2b67b749082a
|
data/.github/CODEOWNERS
CHANGED
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
Utility gem for .github/CODEOWNERS introspection
|
2
2
|
|
3
|
+
GitHub's [CODEOWNERS rules](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners) are allegedly based on the gitignore format with a few small exceptions.
|
4
|
+
|
3
5
|
Install
|
4
6
|
=======
|
5
7
|
|
@@ -14,7 +16,18 @@ Requirements
|
|
14
16
|
Usage
|
15
17
|
=====
|
16
18
|
|
17
|
-
|
19
|
+
```
|
20
|
+
your/repo/path$ code_owners --help
|
21
|
+
usage: code_owners [options]
|
22
|
+
-u, --unowned Display unowned files only
|
23
|
+
-e, --error-unowned Exit with error status if any files are unowned
|
24
|
+
-i, --ignore FILE A file of gitignore patterns to filter out of results, may be specified multiple times, only supported by -n option for now
|
25
|
+
-l, --log Log stuff
|
26
|
+
-n, --no-git [experimental] Use a git-free, pure ruby based implementation
|
27
|
+
-v, --version Display the version of the gem
|
28
|
+
```
|
29
|
+
|
30
|
+
Several of those get transformed into option flags that get passed along to CodeOwners.ownerships, see [bin/codeowners](https://github.com/jcheatham/code_owners/blob/main/bin/code_owners)
|
18
31
|
|
19
32
|
Output
|
20
33
|
======
|
@@ -32,9 +45,9 @@ Maybe put it in a cleanliness test, like:
|
|
32
45
|
|
33
46
|
```ruby
|
34
47
|
it "does not introduce new unowned files" do
|
35
|
-
unowned_files = CodeOwners.ownerships.select { |f| f[:owner] == CodeOwners::NO_OWNER }
|
36
|
-
# this number should only decrease, never increase!
|
37
|
-
assert_equal 12345, unowned_files.count, "Claim ownership of your new files in .github/CODEOWNERS to fix this test!"
|
48
|
+
unowned_files = CodeOwners.ownerships.select { |f| f[:owner] == CodeOwners::NO_OWNER }
|
49
|
+
# this number should only decrease, never increase!
|
50
|
+
assert_equal 12345, unowned_files.count, "Claim ownership of your new files in .github/CODEOWNERS to fix this test!"
|
38
51
|
end
|
39
52
|
```
|
40
53
|
|
data/bin/code_owners
CHANGED
@@ -1,21 +1,11 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
unless system('git --version > /dev/null')
|
4
|
-
STDERR.puts 'Git does not appear to be installed.'
|
5
|
-
exit 2
|
6
|
-
end
|
7
|
-
|
8
|
-
unless system('git rev-parse --is-inside-work-tree > /dev/null')
|
9
|
-
STDERR.puts 'The current working directory must be a Git repo.'
|
10
|
-
exit 3
|
11
|
-
end
|
12
|
-
|
13
3
|
$LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
|
14
4
|
require 'code_owners'
|
15
5
|
require 'code_owners/version'
|
16
6
|
require 'optparse'
|
17
7
|
|
18
|
-
options = {}
|
8
|
+
options = {ignores: []}
|
19
9
|
OptionParser.new do |opts|
|
20
10
|
opts.banner = "usage: code_owners [options]"
|
21
11
|
opts.on('-u', '--unowned', TrueClass, 'Display unowned files only') do |u|
|
@@ -24,14 +14,35 @@ OptionParser.new do |opts|
|
|
24
14
|
opts.on('-e', '--error-unowned', TrueClass, 'Exit with error status if any files are unowned') do |e|
|
25
15
|
options[:error_unowned] = e
|
26
16
|
end
|
17
|
+
opts.on('-i', '--ignore FILE', String, 'A file of gitignore patterns to filter out of results, may be specified multiple times, only supported by -n option for now') do |i|
|
18
|
+
options[:ignores] << i
|
19
|
+
end
|
20
|
+
opts.on('-l', '--log', TrueClass, 'Log stuff') do |l|
|
21
|
+
options[:log] = l
|
22
|
+
end
|
23
|
+
opts.on('-n', '--no-git', TrueClass, '[experimental] Use a git-free, pure ruby based implementation') do |n|
|
24
|
+
options[:no_git] = n
|
25
|
+
end
|
27
26
|
opts.on('-v', '--version', TrueClass, 'Display the version of the gem') do |_|
|
28
27
|
puts "Version: #{CodeOwners::VERSION}"
|
29
28
|
exit 0
|
30
29
|
end
|
31
30
|
end.parse!
|
32
31
|
|
32
|
+
unless options[:no_git]
|
33
|
+
unless system('git --version > /dev/null')
|
34
|
+
STDERR.puts 'Git does not appear to be installed.'
|
35
|
+
exit 2
|
36
|
+
end
|
37
|
+
|
38
|
+
unless system('git rev-parse --is-inside-work-tree > /dev/null')
|
39
|
+
STDERR.puts 'The current working directory must be a Git repo.'
|
40
|
+
exit 3
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
33
44
|
unowned_error = false
|
34
|
-
CodeOwners.ownerships.each do |ownership_status|
|
45
|
+
CodeOwners.ownerships(options).each do |ownership_status|
|
35
46
|
owner_info = ownership_status[:owner].dup
|
36
47
|
if owner_info != CodeOwners::NO_OWNER
|
37
48
|
next if options[:unowned]
|
data/code_owners.gemspec
CHANGED
data/lib/code_owners/version.rb
CHANGED
data/lib/code_owners.rb
CHANGED
@@ -1,30 +1,37 @@
|
|
1
1
|
require "code_owners/version"
|
2
2
|
require "tempfile"
|
3
|
+
require "pathspec"
|
3
4
|
|
4
5
|
module CodeOwners
|
6
|
+
|
5
7
|
NO_OWNER = 'UNOWNED'
|
8
|
+
CODEOWNER_PATTERN = /(.*?)\s+((?:[^\s]*@[^\s]+\s*)+)/
|
9
|
+
POTENTIAL_LOCATIONS = ["CODEOWNERS", "docs/CODEOWNERS", ".github/CODEOWNERS"]
|
10
|
+
|
6
11
|
class << self
|
7
12
|
|
8
|
-
#
|
9
|
-
|
10
|
-
|
11
|
-
#
|
12
|
-
# -c "core.excludesfiles=somefile" -> tells git to use this as our gitignore pattern source
|
13
|
-
# check-ignore -> debug gitignore / exclude files
|
14
|
-
# --no-index -> don't look in the index when checking, can be used to debug why a path became tracked
|
15
|
-
# -v -> verbose, outputs details about the matching pattern (if any) for each given pathname
|
16
|
-
# -n -> non-matching, shows given paths which don't match any pattern
|
17
|
-
|
18
|
-
def log(message)
|
19
|
-
puts message
|
13
|
+
# helper function to create the lookup for when we have a file and want to find its owner
|
14
|
+
def file_ownerships(opts = {})
|
15
|
+
Hash[ ownerships(opts).map { |o| [o[:file], o] } ]
|
20
16
|
end
|
21
17
|
|
22
|
-
|
23
|
-
|
18
|
+
# this maps the collection of ownership patterns and owners to actual files
|
19
|
+
def ownerships(opts = {})
|
20
|
+
log("Calculating ownerships for #{opts.inspect}", opts)
|
21
|
+
patterns = pattern_owners(codeowners_data(opts), opts)
|
22
|
+
if opts[:no_git]
|
23
|
+
files = files_to_own(opts)
|
24
|
+
ownerships_by_ruby(patterns, files, opts)
|
25
|
+
else
|
26
|
+
ownerships_by_gitignore(patterns, opts)
|
27
|
+
end
|
24
28
|
end
|
25
29
|
|
26
|
-
|
27
|
-
|
30
|
+
|
31
|
+
####################
|
32
|
+
# gitignore approach
|
33
|
+
|
34
|
+
def ownerships_by_gitignore(patterns, opts = {})
|
28
35
|
git_owner_info(patterns.map { |p| p[0] }).map do |line, pattern, file|
|
29
36
|
if line.empty?
|
30
37
|
{ file: file, owner: NO_OWNER, line: nil, pattern: nil }
|
@@ -39,35 +46,6 @@ module CodeOwners
|
|
39
46
|
end
|
40
47
|
end
|
41
48
|
|
42
|
-
def search_codeowners_file
|
43
|
-
paths = ["CODEOWNERS", "docs/CODEOWNERS", ".github/CODEOWNERS"]
|
44
|
-
for path in paths
|
45
|
-
current_file_path = File.join(current_repo_path, path)
|
46
|
-
return current_file_path if File.exist?(current_file_path)
|
47
|
-
end
|
48
|
-
abort("[ERROR] CODEOWNERS file does not exist.")
|
49
|
-
end
|
50
|
-
|
51
|
-
# read the github file and spit out a slightly formatted list of patterns and their owners
|
52
|
-
# Empty/invalid/commented lines are still included in order to preserve line numbering
|
53
|
-
def pattern_owners
|
54
|
-
codeowner_path = search_codeowners_file
|
55
|
-
patterns = []
|
56
|
-
File.read(codeowner_path).split("\n").each_with_index { |line, i|
|
57
|
-
path_owner = line.split(/\s+@/, 2)
|
58
|
-
if line.match(/^\s*(?:#.*)?$/)
|
59
|
-
patterns.push ['', ''] # Comment/empty line
|
60
|
-
elsif path_owner.length != 2 || (path_owner[0].empty? && !path_owner[1].empty?)
|
61
|
-
log "Parse error line #{(i+1).to_s}: \"#{line}\""
|
62
|
-
patterns.push ['', ''] # Invalid line
|
63
|
-
else
|
64
|
-
path_owner[1] = '@'+path_owner[1]
|
65
|
-
patterns.push path_owner
|
66
|
-
end
|
67
|
-
}
|
68
|
-
return patterns
|
69
|
-
end
|
70
|
-
|
71
49
|
def git_owner_info(patterns)
|
72
50
|
make_utf8(raw_git_owner_info(patterns)).lines.map do |info|
|
73
51
|
_, _exfile, line, pattern, file = info.strip.match(/^(.*):(\d*):(.*)\t(.*)$/).to_a
|
@@ -75,8 +53,19 @@ module CodeOwners
|
|
75
53
|
end
|
76
54
|
end
|
77
55
|
|
78
|
-
#
|
79
|
-
#
|
56
|
+
# IN: an array of gitignore* check-ignore compliant patterns
|
57
|
+
# OUT: a check-ignore formatted string for each file in the repo
|
58
|
+
#
|
59
|
+
# * https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners#syntax-exceptions
|
60
|
+
# sadly you can't tell ls-files to ignore tracked files via an arbitrary pattern file
|
61
|
+
# so we jump through some hacky git-fu hoops
|
62
|
+
#
|
63
|
+
# -c "core.quotepath=off" ls-files -z # prevent quoting the path and null-terminate each line to assist with matching stuff with spaces
|
64
|
+
# -c "core.excludesfiles=somefile" # tells git to use this as our gitignore pattern source
|
65
|
+
# check-ignore # debug gitignore / exclude files
|
66
|
+
# --no-index # don't look in the index when checking, can be used to debug why a path became tracked
|
67
|
+
# -v # verbose, outputs details about the matching pattern (if any) for each given pathname
|
68
|
+
# -n # non-matching, shows given paths which don't match any pattern
|
80
69
|
def raw_git_owner_info(patterns)
|
81
70
|
Tempfile.open('codeowner_patterns') do |file|
|
82
71
|
file.write(patterns.join("\n"))
|
@@ -85,8 +74,115 @@ module CodeOwners
|
|
85
74
|
end
|
86
75
|
end
|
87
76
|
|
77
|
+
|
78
|
+
###############
|
79
|
+
# ruby approach
|
80
|
+
|
81
|
+
def ownerships_by_ruby(patterns, files, opts = {})
|
82
|
+
ownerships = files.map { |f| { file: f, owner: NO_OWNER, line: nil, pattern: nil } }
|
83
|
+
|
84
|
+
patterns.each_with_index do |(pattern, owner), i|
|
85
|
+
next if pattern == ""
|
86
|
+
pattern = pattern.gsub(/\/\*$/, "/**")
|
87
|
+
spec_pattern = PathSpec::GitIgnoreSpec.new(pattern)
|
88
|
+
ownerships.each do |o|
|
89
|
+
next unless spec_pattern.match(o[:file])
|
90
|
+
o[:owner] = owner
|
91
|
+
o[:line] = i+1
|
92
|
+
o[:pattern] = pattern
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
ownerships
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
|
101
|
+
##############
|
102
|
+
# helper stuff
|
103
|
+
|
104
|
+
# read the github file and spit out a slightly formatted list of patterns and their owners
|
105
|
+
# Empty/invalid/commented lines are still included in order to preserve line numbering
|
106
|
+
def pattern_owners(codeowner_data, opts = {})
|
107
|
+
patterns = []
|
108
|
+
codeowner_data.split("\n").each_with_index do |line, i|
|
109
|
+
stripped_line = line.strip
|
110
|
+
if stripped_line == "" || stripped_line.start_with?("#")
|
111
|
+
patterns << ['', ''] # Comment / empty line
|
112
|
+
|
113
|
+
elsif stripped_line.start_with?("!")
|
114
|
+
# unsupported per github spec
|
115
|
+
log("Parse error line #{(i+1).to_s}: \"#{line}\"", opts)
|
116
|
+
patterns << ['', '']
|
117
|
+
|
118
|
+
elsif stripped_line.match(CODEOWNER_PATTERN)
|
119
|
+
patterns << [$1, $2]
|
120
|
+
|
121
|
+
else
|
122
|
+
log("Parse error line #{(i+1).to_s}: \"#{line}\"", opts)
|
123
|
+
patterns << ['', '']
|
124
|
+
|
125
|
+
end
|
126
|
+
end
|
127
|
+
patterns
|
128
|
+
end
|
129
|
+
|
130
|
+
def log(message, opts = {})
|
131
|
+
puts message if opts[:log]
|
132
|
+
end
|
133
|
+
|
88
134
|
private
|
89
135
|
|
136
|
+
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners#codeowners-file-location
|
137
|
+
# To use a CODEOWNERS file, create a new file called CODEOWNERS in the root, docs/, or .github/ directory of the repository, in the branch where you'd like to add the code owners.
|
138
|
+
|
139
|
+
# if we have access to git, use that to figure out our current repo path and look in there for codeowners
|
140
|
+
# if we don't, this function will attempt to find it while walking back up the directory tree
|
141
|
+
def codeowners_data(opts = {})
|
142
|
+
if opts[:codeowner_data]
|
143
|
+
return opts[:codeowner_data]
|
144
|
+
elsif opts[:codeowner_path]
|
145
|
+
return File.read(opts[:codeowner_path]) if File.exist?(opts[:codeowner_path])
|
146
|
+
elsif opts[:no_git]
|
147
|
+
path = Dir.pwd.split(File::SEPARATOR)
|
148
|
+
while !path.empty?
|
149
|
+
POTENTIAL_LOCATIONS.each do |pl|
|
150
|
+
current_file_path = File.join(path, pl)
|
151
|
+
return File.read(current_file_path) if File.exist?(current_file_path)
|
152
|
+
end
|
153
|
+
path.pop
|
154
|
+
end
|
155
|
+
else
|
156
|
+
path = current_repo_path
|
157
|
+
POTENTIAL_LOCATIONS.each do |pl|
|
158
|
+
current_file_path = File.join(path, pl)
|
159
|
+
return File.read(current_file_path) if File.exist?(current_file_path)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
raise("[ERROR] CODEOWNERS file does not exist.")
|
163
|
+
end
|
164
|
+
|
165
|
+
def files_to_own(opts = {})
|
166
|
+
# glob all files
|
167
|
+
all_files_pattern = File.join("**","**")
|
168
|
+
|
169
|
+
# optionally prefix with list of directories to scope down potential evaluation space
|
170
|
+
if opts[:scoped_dirs]
|
171
|
+
all_files_pattern = File.join("{#{opts[:scoped_dirs].join(",")}}", all_files_pattern)
|
172
|
+
end
|
173
|
+
|
174
|
+
all_files = Dir.glob(all_files_pattern, File::FNM_DOTMATCH)
|
175
|
+
all_files.reject!{|f| f.start_with?(".git/") || File.directory?(f) }
|
176
|
+
|
177
|
+
# filter out ignores if we have them
|
178
|
+
opts[:ignores]&.each do |ignore|
|
179
|
+
ignores = PathSpec.new(File.readlines(ignore, chomp: true).map{|i| i.end_with?("/*") ? "#{i}*" : i })
|
180
|
+
all_files.reject! { |f| ignores.specs.any?{|p| p.match(f) } }
|
181
|
+
end
|
182
|
+
|
183
|
+
all_files
|
184
|
+
end
|
185
|
+
|
90
186
|
def make_utf8(input)
|
91
187
|
input.force_encoding(Encoding::UTF_8)
|
92
188
|
return input if input.valid_encoding?
|
data/spec/code_owners_spec.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'code_owners'
|
2
2
|
require 'tmpdir'
|
3
|
+
require 'json'
|
3
4
|
|
4
5
|
RSpec.describe CodeOwners do |rspec|
|
5
6
|
describe ".file_ownerships" do
|
@@ -20,66 +21,119 @@ RSpec.describe CodeOwners do |rspec|
|
|
20
21
|
end
|
21
22
|
|
22
23
|
describe ".ownerships" do
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
[
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
24
|
+
context "default path shelling out to git" do
|
25
|
+
it "assigns owners to things" do
|
26
|
+
expect(CodeOwners).to receive(:pattern_owners).and_return([["pat1", "own1"], ["pat2*", "own2"], ["pat3", "own3"]])
|
27
|
+
expect(CodeOwners).to receive(:git_owner_info).and_return(
|
28
|
+
[
|
29
|
+
["2", "pat2*", "pat2file"],
|
30
|
+
["", "", "unowned/file"]
|
31
|
+
]
|
32
|
+
)
|
33
|
+
expect(CodeOwners.ownerships).to eq(
|
34
|
+
[
|
35
|
+
{ file: "pat2file", owner: "own2", line: "2", pattern: "pat2*" },
|
36
|
+
{ file: "unowned/file", owner: "UNOWNED", line: nil, pattern: nil }
|
37
|
+
]
|
38
|
+
)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context "using no_git as an option" do
|
43
|
+
it "works" do
|
44
|
+
expect(CodeOwners).to receive(:pattern_owners).and_return([["foo", "own1"], ["foo*", "own2"], ["foo/**", "own3"]])
|
45
|
+
expect(CodeOwners).to receive(:files_to_own).and_return(["zip", "foo.rb", "foo/bar.rb", "foo/bar/baz.rb", "foo/bar/baz/meow.txt", "waffles"])
|
46
|
+
results = CodeOwners.ownerships(no_git: true)
|
47
|
+
expect(results).to match_array([
|
48
|
+
{:file=>"zip", :owner=>"UNOWNED", :line=>nil, :pattern=>nil},
|
49
|
+
{:file=>"foo.rb", :owner=>"own2", :line=>2, :pattern=>"foo*"},
|
50
|
+
{:file=>"foo/bar.rb", :owner=>"own3", :line=>3, :pattern=>"foo/**"},
|
51
|
+
{:file=>"foo/bar/baz.rb", :owner=>"own3", :line=>3, :pattern=>"foo/**"},
|
52
|
+
{:file=>"foo/bar/baz/meow.txt", :owner=>"own3", :line=>3, :pattern=>"foo/**"},
|
53
|
+
{:file=>"waffles", :owner=>"UNOWNED", :line=>nil, :pattern=>nil}
|
54
|
+
])
|
55
|
+
end
|
56
|
+
|
57
|
+
it "behaves as expected of gitignore" do
|
58
|
+
mismatch_count = 0
|
59
|
+
permutations = JSON.parse(File.read("spec/permutations.json"))
|
60
|
+
puts "\nEvaluating #{permutations["permutations"].size} permutations, only printing the mismatches"
|
61
|
+
|
62
|
+
permutations["permutations"].each do |perm, git_matches|
|
63
|
+
expect(CodeOwners).to receive(:pattern_owners).and_return([[perm, "owner"]])
|
64
|
+
expect(CodeOwners).to receive(:files_to_own).and_return(permutations["all_files"])
|
65
|
+
ownerships = CodeOwners.ownerships(no_git: true)
|
66
|
+
spec_matches = ownerships.reject{|o| o[:pattern].nil? }.map{|o| o[:file] }.sort
|
67
|
+
|
68
|
+
diff1 = array_diff(spec_matches, git_matches)
|
69
|
+
diff2 = array_diff(git_matches, spec_matches)
|
70
|
+
unless diff1.empty? && diff2.empty?
|
71
|
+
mismatch_count += 1
|
72
|
+
puts "Permutation #{PathSpec::GitIgnoreSpec.new(perm).inspect}"
|
73
|
+
puts "gitignore matches: #{git_matches}"
|
74
|
+
puts "patchspec matches: #{spec_matches}\n\n"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
puts "Counted #{mismatch_count} mismatches" if mismatch_count > 0
|
78
|
+
end
|
37
79
|
end
|
38
80
|
end
|
39
81
|
|
40
82
|
describe ".pattern_owners" do
|
41
|
-
|
42
|
-
|
43
|
-
@d = d
|
44
|
-
f = File.new(File.join(d, 'CODEOWNERS'), 'w+')
|
45
|
-
f.write <<-CODEOWNERS
|
83
|
+
before do
|
84
|
+
@data = <<-CODEOWNERS
|
46
85
|
lib/* @jcheatham
|
47
86
|
some/path/** @someoneelse
|
48
87
|
other/path/* @someoneelse @anotherperson
|
88
|
+
|
89
|
+
this path/has spaces.txt @spacelover spacer@example.com
|
90
|
+
/this also has spaces.txt spacer@example.com @spacelover
|
91
|
+
|
49
92
|
invalid/code owners/line
|
50
93
|
@AnotherInvalidLine
|
51
94
|
#comment-line (empty line next)
|
95
|
+
!this/is/unsupported.txt @foo
|
96
|
+
here/is/a/valid/path.txt @jcheatham
|
52
97
|
|
53
|
-
|
98
|
+
#/another/comment/line @nobody
|
54
99
|
CODEOWNERS
|
55
|
-
|
56
|
-
|
57
|
-
|
100
|
+
end
|
101
|
+
|
102
|
+
it "returns an empty array given an empty string" do
|
103
|
+
results = CodeOwners.pattern_owners("")
|
104
|
+
expect(results).to eq([])
|
58
105
|
end
|
59
106
|
|
60
107
|
it "returns a list of patterns and owners" do
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
108
|
+
expected_results = [
|
109
|
+
["lib/*", "@jcheatham"],
|
110
|
+
["some/path/**", "@someoneelse"],
|
111
|
+
["other/path/*", "@someoneelse @anotherperson"],
|
112
|
+
["", ""],
|
113
|
+
["this path/has spaces.txt", "@spacelover spacer@example.com"],
|
114
|
+
["/this also has spaces.txt", "spacer@example.com @spacelover"],
|
115
|
+
["", ""],
|
116
|
+
["", ""],
|
117
|
+
["", ""],
|
118
|
+
["", ""],
|
119
|
+
["", ""],
|
120
|
+
["here/is/a/valid/path.txt", "@jcheatham"],
|
121
|
+
["", ""],
|
122
|
+
["", ""]]
|
123
|
+
|
124
|
+
expect(CodeOwners).to receive(:log).exactly(3).times
|
125
|
+
results = CodeOwners.pattern_owners(@data)
|
126
|
+
# do this to compare elements with much nicer failure hints
|
127
|
+
expect(results).to match_array(expected_results)
|
128
|
+
# but do this to guarantee order
|
129
|
+
expect(results).to eq(expected_results)
|
76
130
|
end
|
77
131
|
|
78
132
|
it "prints validation errors and skips lines that aren't the expected format" do
|
79
|
-
expect(CodeOwners).to receive(:
|
80
|
-
expect(CodeOwners).to receive(:log).with("Parse error line
|
81
|
-
expect(CodeOwners).to receive(:log).with("Parse error line
|
82
|
-
pattern_owners = CodeOwners.pattern_owners
|
133
|
+
expect(CodeOwners).to receive(:log).with("Parse error line 8: \"invalid/code owners/line\"", {})
|
134
|
+
expect(CodeOwners).to receive(:log).with("Parse error line 9: \" @AnotherInvalidLine\"", {})
|
135
|
+
expect(CodeOwners).to receive(:log).with("Parse error line 11: \"!this/is/unsupported.txt @foo\"", {})
|
136
|
+
pattern_owners = CodeOwners.pattern_owners(@data)
|
83
137
|
expect(pattern_owners).not_to include(["", "@AnotherInvalidLine"])
|
84
138
|
expect(pattern_owners).to include(["", ""])
|
85
139
|
end
|
@@ -112,6 +166,71 @@ CODEOWNERS
|
|
112
166
|
end
|
113
167
|
end
|
114
168
|
|
169
|
+
describe ".codeowners_data" do
|
170
|
+
context "when passed predefined data" do
|
171
|
+
it "returns the data" do
|
172
|
+
result = CodeOwners.send(:codeowners_data, codeowner_data: "foo")
|
173
|
+
expect(result).to eq("foo")
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
context "when passed a file path" do
|
178
|
+
it "loads the file" do
|
179
|
+
result = CodeOwners.send(:codeowners_data, codeowner_path: ".github/CODEOWNERS")
|
180
|
+
expect(result).to start_with("# This is a CODEOWNERS file.")
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
context "using git" do
|
185
|
+
it "works when in a sub-directory" do
|
186
|
+
Dir.chdir("lib") do
|
187
|
+
result = CodeOwners.send(:codeowners_data)
|
188
|
+
# assuming cloned to a directory named after the repo
|
189
|
+
expect(result).to start_with("# This is a CODEOWNERS file.")
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
it "fails when not in a repo" do
|
194
|
+
Dir.chdir("/") do
|
195
|
+
# this should also print out an error to stderror along the lines of
|
196
|
+
# fatal: not a git repository (or any of the parent directories): .git
|
197
|
+
expect { CodeOwners.send(:codeowners_data) }.to raise_error(RuntimeError)
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
context "not using git" do
|
203
|
+
it "works when in a sub-directory" do
|
204
|
+
Dir.chdir("lib") do
|
205
|
+
result = CodeOwners.send(:codeowners_data, no_git: true)
|
206
|
+
# assuming cloned to a directory named after the repo
|
207
|
+
expect(result).to start_with("# This is a CODEOWNERS file.")
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
it "fails when not in a repo" do
|
212
|
+
Dir.chdir("/") do
|
213
|
+
expect { CodeOwners.send(:codeowners_data, no_git: true) }.to raise_error(RuntimeError)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
describe ".files_to_own" do
|
220
|
+
it "returns all files" do
|
221
|
+
result = CodeOwners.send(:files_to_own)
|
222
|
+
expect(result).to include('Gemfile')
|
223
|
+
expect(result).to include('lib/code_owners.rb')
|
224
|
+
expect(result).to include('spec/files/foo/fake_gem.gem')
|
225
|
+
end
|
226
|
+
|
227
|
+
it "removes ignored files" do
|
228
|
+
result = CodeOwners.send(:files_to_own, ignores: [".gitignore"])
|
229
|
+
expect(result).to include('spec/files/foo/bar/baz/baz.txt')
|
230
|
+
expect(result).not_to include('spec/files/foo/fake_gem.gem')
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
115
234
|
describe "code_owners" do
|
116
235
|
VERSION_REGEX = /Version: \d+\.\d+\.\d+(-[a-z0-9]+)?/i
|
117
236
|
|
@@ -123,4 +242,16 @@ CODEOWNERS
|
|
123
242
|
expect(`bin#{File::SEPARATOR}code_owners --version`).to match VERSION_REGEX
|
124
243
|
end
|
125
244
|
end
|
245
|
+
|
246
|
+
# yoinked from rspec match_array
|
247
|
+
def array_diff(array_1, array_2)
|
248
|
+
difference = array_1.dup
|
249
|
+
array_2.each do |element|
|
250
|
+
if index = difference.index(element)
|
251
|
+
difference.delete_at(index)
|
252
|
+
end
|
253
|
+
end
|
254
|
+
difference
|
255
|
+
end
|
256
|
+
|
126
257
|
end
|
@@ -0,0 +1 @@
|
|
1
|
+
just git ignore things
|
@@ -0,0 +1 @@
|
|
1
|
+
This is afoodle.txt
|
@@ -0,0 +1 @@
|
|
1
|
+
This is serious business
|
@@ -0,0 +1 @@
|
|
1
|
+
blah blah blah
|
@@ -0,0 +1 @@
|
|
1
|
+
This is bar
|
@@ -0,0 +1 @@
|
|
1
|
+
This is bar.txt
|
@@ -0,0 +1 @@
|
|
1
|
+
This is spec/files/foo/bar/baz/baz.txt
|
@@ -0,0 +1 @@
|
|
1
|
+
This is baz2.txt
|
@@ -0,0 +1 @@
|
|
1
|
+
This is some bar.txt
|