code_owners 1.0.9 → 2.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 +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
|