code_owners 1.0.7 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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*"},
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
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,5 +157,101 @@ 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
89
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
232
+ end
233
+
234
+ describe "code_owners" do
235
+ VERSION_REGEX = /Version: \d+\.\d+\.\d+(-[a-z0-9]+)?/i
236
+
237
+ it "prints a version number with the short option" do
238
+ expect(`bin#{File::SEPARATOR}code_owners -v`).to match VERSION_REGEX
239
+ end
240
+
241
+ it "prints a version number with the short option" do
242
+ expect(`bin#{File::SEPARATOR}code_owners --version`).to match VERSION_REGEX
243
+ end
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
+
90
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 @@
1
+ ☃☃☃☃☃☃☃☃☃
@@ -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