code_owners 1.0.8 → 2.0.1
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 +15 -13
- 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 +157 -46
- data/spec/code_owners_spec.rb +194 -39
- 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/file name.txt +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 +33 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 42ad83e6c56f3bb34904d52dde4eb0f689fde59ea59868ecd4c428f3f13c8762
|
4
|
+
data.tar.gz: f8abb4f85d6fdf586432d047d9c24a521ea4411853a4a36e88de57556698d6a1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 80e88fb5714e128bcb401a8ecd7be79e631c3105c370d7ac275bc2ffd60ac92182b5d6090ad60782cba0cc6af89a60a0758466d4ebc703269ad83ea8d8cf5e4e
|
7
|
+
data.tar.gz: fa22fe034e7256b8637564a74803c044e85c675399978e8ccfc4d114b1b88d958501f5143c205d8cc159c40a1f823f269567cf6e22a84df896e5161f6a385113
|
data/.github/CODEOWNERS
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,27 +1,29 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
code_owners (
|
4
|
+
code_owners (2.0.1)
|
5
|
+
pathspec
|
5
6
|
rake
|
6
7
|
|
7
8
|
GEM
|
8
9
|
remote: http://rubygems.org/
|
9
10
|
specs:
|
10
|
-
diff-lcs (1.
|
11
|
+
diff-lcs (1.5.0)
|
12
|
+
pathspec (0.2.1)
|
11
13
|
rake (13.0.6)
|
12
|
-
rspec (3.
|
13
|
-
rspec-core (~> 3.
|
14
|
-
rspec-expectations (~> 3.
|
15
|
-
rspec-mocks (~> 3.
|
16
|
-
rspec-core (3.
|
17
|
-
rspec-support (~> 3.
|
18
|
-
rspec-expectations (3.
|
14
|
+
rspec (3.11.0)
|
15
|
+
rspec-core (~> 3.11.0)
|
16
|
+
rspec-expectations (~> 3.11.0)
|
17
|
+
rspec-mocks (~> 3.11.0)
|
18
|
+
rspec-core (3.11.0)
|
19
|
+
rspec-support (~> 3.11.0)
|
20
|
+
rspec-expectations (3.11.0)
|
19
21
|
diff-lcs (>= 1.2.0, < 2.0)
|
20
|
-
rspec-support (~> 3.
|
21
|
-
rspec-mocks (3.
|
22
|
+
rspec-support (~> 3.11.0)
|
23
|
+
rspec-mocks (3.11.1)
|
22
24
|
diff-lcs (>= 1.2.0, < 2.0)
|
23
|
-
rspec-support (~> 3.
|
24
|
-
rspec-support (3.
|
25
|
+
rspec-support (~> 3.11.0)
|
26
|
+
rspec-support (3.11.0)
|
25
27
|
|
26
28
|
PLATFORMS
|
27
29
|
ruby
|
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,26 +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
|
-
|
13
|
-
#
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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] } ]
|
16
|
+
end
|
17
|
+
|
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
|
+
patowns = pattern_owners(codeowners_data(opts), opts)
|
22
|
+
if opts[:no_git]
|
23
|
+
files = files_to_own(opts)
|
24
|
+
ownerships_by_ruby(patowns, files, opts)
|
25
|
+
else
|
26
|
+
ownerships_by_gitignore(patowns, opts)
|
27
|
+
end
|
20
28
|
end
|
21
29
|
|
22
|
-
|
23
|
-
|
30
|
+
|
31
|
+
####################
|
32
|
+
# gitignore approach
|
33
|
+
|
34
|
+
def ownerships_by_gitignore(patterns, opts = {})
|
24
35
|
git_owner_info(patterns.map { |p| p[0] }).map do |line, pattern, file|
|
25
36
|
if line.empty?
|
26
37
|
{ file: file, owner: NO_OWNER, line: nil, pattern: nil }
|
@@ -35,35 +46,6 @@ module CodeOwners
|
|
35
46
|
end
|
36
47
|
end
|
37
48
|
|
38
|
-
def search_codeowners_file
|
39
|
-
paths = ["CODEOWNERS", "docs/CODEOWNERS", ".github/CODEOWNERS"]
|
40
|
-
for path in paths
|
41
|
-
current_file_path = File.join(current_repo_path, path)
|
42
|
-
return current_file_path if File.exist?(current_file_path)
|
43
|
-
end
|
44
|
-
abort("[ERROR] CODEOWNERS file does not exist.")
|
45
|
-
end
|
46
|
-
|
47
|
-
# read the github file and spit out a slightly formatted list of patterns and their owners
|
48
|
-
# Empty/invalid/commented lines are still included in order to preserve line numbering
|
49
|
-
def pattern_owners
|
50
|
-
codeowner_path = search_codeowners_file
|
51
|
-
patterns = []
|
52
|
-
File.read(codeowner_path).split("\n").each_with_index { |line, i|
|
53
|
-
path_owner = line.split(/\s+@/, 2)
|
54
|
-
if line.match(/^\s*(?:#.*)?$/)
|
55
|
-
patterns.push ['', ''] # Comment/empty line
|
56
|
-
elsif path_owner.length != 2 || (path_owner[0].empty? && !path_owner[1].empty?)
|
57
|
-
log "Parse error line #{(i+1).to_s}: \"#{line}\""
|
58
|
-
patterns.push ['', ''] # Invalid line
|
59
|
-
else
|
60
|
-
path_owner[1] = '@'+path_owner[1]
|
61
|
-
patterns.push path_owner
|
62
|
-
end
|
63
|
-
}
|
64
|
-
return patterns
|
65
|
-
end
|
66
|
-
|
67
49
|
def git_owner_info(patterns)
|
68
50
|
make_utf8(raw_git_owner_info(patterns)).lines.map do |info|
|
69
51
|
_, _exfile, line, pattern, file = info.strip.match(/^(.*):(\d*):(.*)\t(.*)$/).to_a
|
@@ -71,18 +53,147 @@ module CodeOwners
|
|
71
53
|
end
|
72
54
|
end
|
73
55
|
|
74
|
-
#
|
75
|
-
#
|
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
|
76
69
|
def raw_git_owner_info(patterns)
|
77
70
|
Tempfile.open('codeowner_patterns') do |file|
|
78
71
|
file.write(patterns.join("\n"))
|
79
72
|
file.rewind
|
80
|
-
`cd #{current_repo_path} && git -c \"core.quotepath=off\" ls-files | xargs -- git -c \"core.quotepath=off\" -c \"core.excludesfile=#{file.path}\" check-ignore --no-index -v -n`
|
73
|
+
`cd #{current_repo_path} && git -c \"core.quotepath=off\" ls-files -z | xargs -0 -- git -c \"core.quotepath=off\" -c \"core.excludesfile=#{file.path}\" check-ignore --no-index -v -n`
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
###############
|
79
|
+
# ruby approach
|
80
|
+
|
81
|
+
def ownerships_by_ruby(patowns, files, opts = {})
|
82
|
+
pattern_list = build_ruby_patterns(patowns, opts)
|
83
|
+
unowned = { owner: NO_OWNER, line: nil, pattern: nil }
|
84
|
+
|
85
|
+
files.map do |file|
|
86
|
+
last_match = nil
|
87
|
+
# have a flag to go through in reverse order as potential optimization?
|
88
|
+
# really depends on the data
|
89
|
+
pattern_list.each do |p|
|
90
|
+
last_match = p if p[:pattern_regex].match(file)
|
91
|
+
end
|
92
|
+
(last_match || unowned).dup.tap{|h| h[:file] = file }
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def build_ruby_patterns(patowns, opts = {})
|
97
|
+
pattern_list = []
|
98
|
+
patowns.each_with_index do |(pattern, owner), i|
|
99
|
+
next if pattern == ""
|
100
|
+
pattern_list << {
|
101
|
+
owner: owner,
|
102
|
+
line: i+1,
|
103
|
+
pattern: pattern,
|
104
|
+
# gsub because spec approach needs a little help matching remainder of tree recursively
|
105
|
+
pattern_regex: PathSpec::GitIgnoreSpec.new(pattern.gsub(/\/\*$/, "/**"))
|
106
|
+
}
|
81
107
|
end
|
108
|
+
pattern_list
|
109
|
+
end
|
110
|
+
|
111
|
+
|
112
|
+
##############
|
113
|
+
# helper stuff
|
114
|
+
|
115
|
+
# read the github file and spit out a slightly formatted list of patterns and their owners
|
116
|
+
# Empty/invalid/commented lines are still included in order to preserve line numbering
|
117
|
+
def pattern_owners(codeowner_data, opts = {})
|
118
|
+
patterns = []
|
119
|
+
codeowner_data.split("\n").each_with_index do |line, i|
|
120
|
+
stripped_line = line.strip
|
121
|
+
if stripped_line == "" || stripped_line.start_with?("#")
|
122
|
+
patterns << ['', ''] # Comment / empty line
|
123
|
+
|
124
|
+
elsif stripped_line.start_with?("!")
|
125
|
+
# unsupported per github spec
|
126
|
+
log("Parse error line #{(i+1).to_s}: \"#{line}\"", opts)
|
127
|
+
patterns << ['', '']
|
128
|
+
|
129
|
+
elsif stripped_line.match(CODEOWNER_PATTERN)
|
130
|
+
patterns << [$1, $2]
|
131
|
+
|
132
|
+
else
|
133
|
+
log("Parse error line #{(i+1).to_s}: \"#{line}\"", opts)
|
134
|
+
patterns << ['', '']
|
135
|
+
|
136
|
+
end
|
137
|
+
end
|
138
|
+
patterns
|
139
|
+
end
|
140
|
+
|
141
|
+
def log(message, opts = {})
|
142
|
+
puts message if opts[:log]
|
82
143
|
end
|
83
144
|
|
84
145
|
private
|
85
146
|
|
147
|
+
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners#codeowners-file-location
|
148
|
+
# 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.
|
149
|
+
|
150
|
+
# if we have access to git, use that to figure out our current repo path and look in there for codeowners
|
151
|
+
# if we don't, this function will attempt to find it while walking back up the directory tree
|
152
|
+
def codeowners_data(opts = {})
|
153
|
+
if opts[:codeowner_data]
|
154
|
+
return opts[:codeowner_data]
|
155
|
+
elsif opts[:codeowner_path]
|
156
|
+
return File.read(opts[:codeowner_path]) if File.exist?(opts[:codeowner_path])
|
157
|
+
elsif opts[:no_git]
|
158
|
+
path = Dir.pwd.split(File::SEPARATOR)
|
159
|
+
while !path.empty?
|
160
|
+
POTENTIAL_LOCATIONS.each do |pl|
|
161
|
+
current_file_path = File.join(path, pl)
|
162
|
+
return File.read(current_file_path) if File.exist?(current_file_path)
|
163
|
+
end
|
164
|
+
path.pop
|
165
|
+
end
|
166
|
+
else
|
167
|
+
path = current_repo_path
|
168
|
+
POTENTIAL_LOCATIONS.each do |pl|
|
169
|
+
current_file_path = File.join(path, pl)
|
170
|
+
return File.read(current_file_path) if File.exist?(current_file_path)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
raise("[ERROR] CODEOWNERS file does not exist.")
|
174
|
+
end
|
175
|
+
|
176
|
+
def files_to_own(opts = {})
|
177
|
+
# glob all files
|
178
|
+
all_files_pattern = File.join("**","**")
|
179
|
+
|
180
|
+
# optionally prefix with list of directories to scope down potential evaluation space
|
181
|
+
if opts[:scoped_dirs]
|
182
|
+
all_files_pattern = File.join("{#{opts[:scoped_dirs].join(",")}}", all_files_pattern)
|
183
|
+
end
|
184
|
+
|
185
|
+
all_files = Dir.glob(all_files_pattern, File::FNM_DOTMATCH)
|
186
|
+
all_files.reject!{|f| f.start_with?(".git/") || File.directory?(f) }
|
187
|
+
|
188
|
+
# filter out ignores if we have them
|
189
|
+
opts[:ignores]&.each do |ignore|
|
190
|
+
ignores = PathSpec.new(File.readlines(ignore, chomp: true).map{|i| i.end_with?("/*") ? "#{i}*" : i })
|
191
|
+
all_files.reject! { |f| ignores.specs.any?{|p| p.match(f) } }
|
192
|
+
end
|
193
|
+
|
194
|
+
all_files
|
195
|
+
end
|
196
|
+
|
86
197
|
def make_utf8(input)
|
87
198
|
input.force_encoding(Encoding::UTF_8)
|
88
199
|
return input if input.valid_encoding?
|