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.
@@ -1,68 +1,139 @@
1
1
  require 'code_owners'
2
2
  require 'tmpdir'
3
+ require 'json'
3
4
 
4
5
  RSpec.describe CodeOwners do |rspec|
5
- describe ".ownerships" do
6
- it "assigns owners to things" do
7
- expect(CodeOwners).to receive(:pattern_owners).and_return([["pat1", "own1"], ["pat2*", "own2"], ["pat3", "own3"]])
8
- expect(CodeOwners).to receive(:git_owner_info).and_return(
9
- [
10
- ["2", "pat2*", "pat2file"],
11
- ["", "", "unowned/file"]
12
- ]
13
- )
14
- expect(CodeOwners.ownerships).to eq(
6
+ describe ".file_ownerships" do
7
+ it "returns a hash of ownerships keyed by file path" do
8
+ expect(CodeOwners).to receive(:ownerships).and_return(
15
9
  [
16
10
  { file: "pat2file", owner: "own2", line: "2", pattern: "pat2*" },
17
11
  { file: "unowned/file", owner: "UNOWNED", line: nil, pattern: nil }
18
12
  ]
19
13
  )
14
+ expect(CodeOwners.file_ownerships).to eq(
15
+ {
16
+ "pat2file" => { file: "pat2file", owner: "own2", line: "2", pattern: "pat2*" },
17
+ "unowned/file" => { file: "unowned/file", owner: "UNOWNED", line: nil, pattern: nil }
18
+ }
19
+ )
20
+ end
21
+ end
22
+
23
+ describe ".ownerships" do
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*", pattern_regex: anything},
50
+ {:file=>"foo/bar.rb", :owner=>"own3", :line=>3, :pattern=>"foo/**", pattern_regex: anything},
51
+ {:file=>"foo/bar/baz.rb", :owner=>"own3", :line=>3, :pattern=>"foo/**", pattern_regex: anything},
52
+ {:file=>"foo/bar/baz/meow.txt", :owner=>"own3", :line=>3, :pattern=>"foo/**", pattern_regex: anything},
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
20
79
  end
21
80
  end
22
81
 
23
82
  describe ".pattern_owners" do
24
- around(:each) do |example|
25
- Dir.mktmpdir { |d|
26
- @d = d
27
- f = File.new(File.join(d, 'CODEOWNERS'), 'w+')
28
- f.write <<-CODEOWNERS
83
+ before do
84
+ @data = <<-CODEOWNERS
29
85
  lib/* @jcheatham
30
86
  some/path/** @someoneelse
31
87
  other/path/* @someoneelse @anotherperson
32
- invalid/codeowners/line
88
+
89
+ this path/has spaces.txt @spacelover spacer@example.com
90
+ /this also has spaces.txt spacer@example.com @spacelover
91
+
92
+ invalid/code owners/line
33
93
  @AnotherInvalidLine
34
94
  #comment-line (empty line next)
95
+ !this/is/unsupported.txt @foo
96
+ here/is/a/valid/path.txt @jcheatham
35
97
 
36
- # another comment line
98
+ #/another/comment/line @nobody
37
99
  CODEOWNERS
38
- f.close
39
- example.run
40
- }
100
+ end
101
+
102
+ it "returns an empty array given an empty string" do
103
+ results = CodeOwners.pattern_owners("")
104
+ expect(results).to eq([])
41
105
  end
42
106
 
43
107
  it "returns a list of patterns and owners" do
44
- expect(CodeOwners).to receive(:current_repo_path).and_return(@d)
45
- expect(CodeOwners).to receive(:log).twice
46
- pattern_owners = CodeOwners.pattern_owners
47
- expect(pattern_owners).to include(["other/path/*", "@someoneelse @anotherperson"])
48
- end
49
-
50
- it "works when invoked in a repo's subdirectory" do
51
- expect(CodeOwners).to receive(:current_repo_path).and_return(@d)
52
- expect(CodeOwners).to receive(:log).twice
53
- subdir = File.join(@d, 'spec')
54
- Dir.mkdir(subdir)
55
- Dir.chdir(subdir) do
56
- pattern_owners = CodeOwners.pattern_owners
57
- expect(pattern_owners).to include(["lib/*", "@jcheatham"])
58
- 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)
59
130
  end
60
131
 
61
132
  it "prints validation errors and skips lines that aren't the expected format" do
62
- expect(CodeOwners).to receive(:current_repo_path).and_return(@d)
63
- expect(CodeOwners).to receive(:log).with("Parse error line 4: \"invalid/codeowners/line \"")
64
- expect(CodeOwners).to receive(:log).with("Parse error line 5: \" @AnotherInvalidLine\"")
65
- 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)
66
137
  expect(pattern_owners).not_to include(["", "@AnotherInvalidLine"])
67
138
  expect(pattern_owners).to include(["", ""])
68
139
  end
@@ -86,6 +157,78 @@ CODEOWNERS
86
157
  expect(raw_ownership.size).to be >= 1
87
158
  expect(raw_ownership).to match(/^(?:.*:\d*:.*\t.*\n)+$/)
88
159
  end
160
+
161
+ context "when path includes a space" do
162
+ it "returns the path in single line" do
163
+ raw_ownership = CodeOwners.raw_git_owner_info(["/spec/files/*"])
164
+ expect(raw_ownership).to match(/.+:\d+:.+\tspec\/files\/file name\.txt\n/)
165
+ end
166
+ end
167
+ end
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
89
232
  end
90
233
 
91
234
  describe "code_owners" do
@@ -99,4 +242,16 @@ CODEOWNERS
99
242
  expect(`bin#{File::SEPARATOR}code_owners --version`).to match VERSION_REGEX
100
243
  end
101
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
+
102
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 file's name includes a space.
@@ -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
@@ -0,0 +1 @@
1
+ This is confoozing.txt
@@ -0,0 +1 @@
1
+ This is a fake gem to trigger .gitignore
@@ -0,0 +1 @@
1
+ this is a foo file
@@ -0,0 +1 @@
1
+ this is foo.txt
@@ -0,0 +1 @@
1
+ true facts
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'code_owners'
4
+ require 'tmpdir'
5
+ require 'json'
6
+ require 'pathname'
7
+ require 'byebug'
8
+
9
+ output = { permutations: {} }
10
+
11
+ def reset_file_to(file, content)
12
+ file.rewind
13
+ file.write(content)
14
+ file.flush
15
+ file.truncate(file.pos)
16
+ end
17
+
18
+ # UGGGGGH
19
+ # so, turns out, the part in git-scm where it says
20
+ # > A trailing "/**" matches everything inside. For example, "abc/**" matches all files inside directory "abc", relative to the location of the .gitignore file, with infinite depth.
21
+ # that "relative" bit gets REAL obnoxious when it comes to trying to evaluate using an adhoc core.excludesFile directive
22
+ # speaking of, it seems like if you have one defined in the tree of a project git will STILL use that
23
+ # see the special exception case mentioned below for the *.gem pattern conflicting with the project's .gitignore
24
+ # there was no way to replace/unset it I could find that doesn't affect something more permanent >:/
25
+
26
+ ignore_file = File.open("spec/files/.gitignore", "w+")
27
+
28
+ Dir.chdir("spec/files") do
29
+ all_files = Dir.glob(File.join("**","**"), File::FNM_DOTMATCH)
30
+ all_files.reject! { |f| File.directory?(f) }
31
+ output[:all_files] = all_files
32
+
33
+ permutables = ["", "*", ".", "/", "*/", "/*", "**", "**/", "/**", "**/**", "*/**", "**/*"]
34
+ ignore_cases = permutables.map do |p1|
35
+ ["foo", "bar", "foo/bar"].map do |p2|
36
+ permutables.map do |p3|
37
+ "#{p1}#{p2}#{p3}"
38
+ end
39
+ end
40
+ end.flatten
41
+
42
+ # can add more one-off cases we want to evaluate here
43
+ ignore_cases << ".dot*"
44
+
45
+ ignore_cases.sort!
46
+ puts "Evaluating #{ignore_cases.size} permutations"
47
+
48
+ rootpath = Pathname.new(".")
49
+
50
+ ignore_cases.each do |perm|
51
+ puts "evaluating #{perm}"
52
+ reset_file_to(ignore_file, perm)
53
+ ignore_matches = []
54
+ ignore_results = `find . -type f | sed "s|^\./||" | tr '\n' '\\0' | xargs -0 -- git -c "core.quotepath=off" check-ignore --no-index -v -n`
55
+ ignore_results.scan(/^([^:]+):(\d+):([^\t]*)\t(.*)/).each do |m_source, m_line, m_pattern, m_file|
56
+ if m_source != ignore_file.path || m_line != "1" || m_pattern != perm
57
+ next if m_source == ".gitignore" && m_line == "1" && m_pattern == "*.gem"
58
+ puts "ERROR?!"
59
+ puts "expecting #{ignore_file.path.inspect}, got #{m_source.inspect}"
60
+ puts "expecting 1, got #{m_line.inspect}"
61
+ puts "expecting #{perm.inspect}, got #{m_pattern.inspect}"
62
+ puts ignore_results
63
+ end
64
+ ignore_matches << Pathname.new(m_file).relative_path_from(rootpath).to_s
65
+ end
66
+ output[:permutations][perm] = ignore_matches.sort
67
+ end
68
+
69
+ reset_file_to(ignore_file, "blah blah blah")
70
+ end
71
+
72
+ File.open("spec/permutations.json", "w+") do |perm_file|
73
+ perm_file.write(JSON.pretty_generate(output))
74
+ end