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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: add8d9474667ea07ea6ad3e207e9a0e085109ac9
4
- data.tar.gz: 86f55b021ef6314614ac2e5f2a59dfec43adbcd1
2
+ SHA256:
3
+ metadata.gz: baea109754326800382bc8665df1e20634680187a1209f612bcc14f1447a962e
4
+ data.tar.gz: 25975f589029d740031c6c14832341a605fa885386be3f88bcb992cb427b1bf0
5
5
  SHA512:
6
- metadata.gz: 6bc09a62ef1d0b86e7a9be070b27c5ed0e21be5b797f29b2597b3c06ffecc8210fd50906319f67028bc8e8a4df3aa2b575ab5ae49a083f2af029f43182d9d074
7
- data.tar.gz: 62e13f5c44667eff192eabb510dca677a2088e58ce123e3dbaf4ccd16c8ebf9eb1dffa8125d5524161de36df730b9eea83093776ea6a55fb40c72df6f741eec3
6
+ metadata.gz: be4b52ceca387c455f3b597f47c3f61aed0d3c381eb4d52e6f2d9c25311d0505704ebb817af0ab73892521bdb1f804fc3a560bfb49c54e1f62b03bf20fe6b796
7
+ data.tar.gz: b31b2929d9023a3609bbd8286b4579ff87c8af5f4a6fc9025ccce9f8bdbddb65c09facf92c7a6873dd20423e925e083de41ba7de8800210ea2cf2b67b749082a
data/.github/CODEOWNERS CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  # empty line followed by a comment?! madness
6
6
  lib/* @jcheatham
7
+ **/bar/** @jcheatham
7
8
  spec/* @jcheatham
8
9
  Rakefile @jcheatham
9
10
 
data/Gemfile.lock CHANGED
@@ -1,13 +1,15 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- code_owners (1.0.9)
4
+ code_owners (2.0.0)
5
+ pathspec
5
6
  rake
6
7
 
7
8
  GEM
8
9
  remote: http://rubygems.org/
9
10
  specs:
10
11
  diff-lcs (1.3)
12
+ pathspec (0.2.1)
11
13
  rake (13.0.6)
12
14
  rspec (3.8.0)
13
15
  rspec-core (~> 3.8.0)
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
- your/repo/path$ code_owners
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
@@ -19,4 +19,5 @@ Gem::Specification.new name, CodeOwners::VERSION do |s|
19
19
 
20
20
  s.add_development_dependency "rspec"
21
21
  s.add_runtime_dependency "rake"
22
+ s.add_runtime_dependency "pathspec"
22
23
  end
@@ -1,3 +1,3 @@
1
1
  module CodeOwners
2
- VERSION = "1.0.9"
2
+ VERSION = "2.0.0"
3
3
  end
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
- # github's CODEOWNERS rules (https://help.github.com/articles/about-codeowners/) are allegedly based on the gitignore format.
9
- # but you can't tell ls-files to ignore tracked files via an arbitrary pattern file
10
- # so we need to jump through some hacky git-fu hoops
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
- def file_ownerships
23
- Hash[ ownerships.map { |o| [o[:file], o] } ]
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
- def ownerships
27
- patterns = pattern_owners
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
- # expects an array of gitignore compliant patterns
79
- # generates a check-ignore formatted string for each file in the repo
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?
@@ -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
- it "assigns owners to things" do
24
- expect(CodeOwners).to receive(:pattern_owners).and_return([["pat1", "own1"], ["pat2*", "own2"], ["pat3", "own3"]])
25
- expect(CodeOwners).to receive(:git_owner_info).and_return(
26
- [
27
- ["2", "pat2*", "pat2file"],
28
- ["", "", "unowned/file"]
29
- ]
30
- )
31
- expect(CodeOwners.ownerships).to eq(
32
- [
33
- { file: "pat2file", owner: "own2", line: "2", pattern: "pat2*" },
34
- { file: "unowned/file", owner: "UNOWNED", line: nil, pattern: nil }
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
- around(:each) do |example|
42
- Dir.mktmpdir { |d|
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
- # another comment line
98
+ #/another/comment/line @nobody
54
99
  CODEOWNERS
55
- f.close
56
- example.run
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
- expect(CodeOwners).to receive(:current_repo_path).and_return(@d)
62
- expect(CodeOwners).to receive(:log).twice
63
- pattern_owners = CodeOwners.pattern_owners
64
- expect(pattern_owners).to include(["other/path/*", "@someoneelse @anotherperson"])
65
- end
66
-
67
- it "works when invoked in a repo's subdirectory" do
68
- expect(CodeOwners).to receive(:current_repo_path).and_return(@d)
69
- expect(CodeOwners).to receive(:log).twice
70
- subdir = File.join(@d, 'spec')
71
- Dir.mkdir(subdir)
72
- Dir.chdir(subdir) do
73
- pattern_owners = CodeOwners.pattern_owners
74
- expect(pattern_owners).to include(["lib/*", "@jcheatham"])
75
- end
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(:current_repo_path).and_return(@d)
80
- expect(CodeOwners).to receive(:log).with("Parse error line 4: \"invalid/code owners/line\"")
81
- expect(CodeOwners).to receive(:log).with("Parse error line 5: \" @AnotherInvalidLine\"")
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