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 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