Skip to content

Commit 26f6c76

Browse files
tstannardperryqh
andcommitted
refactoring file is directory ownership
Co-authored-by: Perry Hertler <[email protected]> Co-authored-by: Teal Stannard <[email protected]> Co-authored-by: Perry Hertler <[email protected]>
1 parent bd75d69 commit 26f6c76

File tree

6 files changed

+181
-59
lines changed

6 files changed

+181
-59
lines changed

code_ownership.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Gem::Specification.new do |spec|
22
spec.name = 'code_ownership'
3-
spec.version = '1.36.0'
3+
spec.version = '1.36.1'
44
spec.authors = ['Gusto Engineers']
55
spec.email = ['[email protected]']
66
spec.summary = 'A gem to help engineering teams declare ownership of code'

lib/code_ownership/private.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
require 'code_ownership/private/validations/files_have_unique_owners'
1414
require 'code_ownership/private/ownership_mappers/file_annotations'
1515
require 'code_ownership/private/ownership_mappers/team_globs'
16+
require 'code_ownership/private/ownership_mappers/file_owner'
1617
require 'code_ownership/private/ownership_mappers/directory_ownership'
1718
require 'code_ownership/private/ownership_mappers/package_ownership'
1819
require 'code_ownership/private/ownership_mappers/js_package_ownership'

lib/code_ownership/private/ownership_mappers/directory_ownership.rb

Lines changed: 11 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,21 @@ class DirectoryOwnership
1010
include Mapper
1111

1212
CODEOWNERS_DIRECTORY_FILE_NAME = '.codeowner'
13-
RELATIVE_ROOT = Pathname('.').freeze
14-
ABSOLUTE_ROOT = Pathname('/').freeze
1513

1614
@@directory_cache = T.let({}, T::Hash[String, T.nilable(CodeTeams::Team)]) # rubocop:disable Style/ClassVars
1715

1816
sig do
19-
override.params(file: String).
20-
returns(T.nilable(::CodeTeams::Team))
17+
override.params(file: String)
18+
.returns(T.nilable(::CodeTeams::Team))
2119
end
2220
def map_file_to_owner(file)
2321
map_file_to_relevant_owner(file)
2422
end
2523

2624
sig do
27-
override.params(cache: GlobsToOwningTeamMap, files: T::Array[String]).returns(GlobsToOwningTeamMap)
25+
override.params(_cache: GlobsToOwningTeamMap, files: T::Array[String]).returns(GlobsToOwningTeamMap)
2826
end
29-
def update_cache(cache, files)
27+
def update_cache(_cache, files)
3028
globs_to_owner(files)
3129
end
3230

@@ -39,17 +37,17 @@ def update_cache(cache, files)
3937
# subset of files, but rather we want code ownership for all files.
4038
#
4139
sig do
42-
override.params(files: T::Array[String]).
43-
returns(T::Hash[String, ::CodeTeams::Team])
40+
override.params(_files: T::Array[String])
41+
.returns(T::Hash[String, ::CodeTeams::Team])
4442
end
45-
def globs_to_owner(files)
43+
def globs_to_owner(_files)
4644
# The T.unsafe is because the upstream RBI is wrong for Pathname.glob
4745
T
4846
.unsafe(Pathname)
4947
.glob(File.join('**/', CODEOWNERS_DIRECTORY_FILE_NAME))
5048
.map(&:cleanpath)
5149
.each_with_object({}) do |pathname, res|
52-
owner = owner_for_codeowners_file(pathname)
50+
owner = FileOwner.owner_for_codeowners_file(pathname)
5351
res[pathname.dirname.cleanpath.join('**/**').to_s] = owner
5452
end
5553
end
@@ -66,56 +64,13 @@ def bust_caches!
6664

6765
private
6866

69-
sig { params(codeowners_file: Pathname).returns(CodeTeams::Team) }
70-
def owner_for_codeowners_file(codeowners_file)
71-
raw_owner_value = File.foreach(codeowners_file).first.strip
72-
73-
Private.find_team!(
74-
raw_owner_value,
75-
codeowners_file.to_s
76-
)
77-
end
78-
79-
# Takes a file and finds the relevant `.codeowner` file by walking up the directory
67+
# takes a file and finds the relevant `.codeowner` file by walking up the directory
8068
# structure. Example, given `a/b/c.rb`, this looks for `a/b/.codeowner`, `a/.codeowner`,
8169
# and `.codeowner` in that order, stopping at the first file to actually exist.
82-
# If the parovided file is a directory, it will look for `.codeowner` in that directory and then upwards.
83-
# We do additional caching so that we don't have to check for file existence every time.
70+
# We do additional caching so that we don't have to check for file existence every time
8471
sig { params(file: String).returns(T.nilable(CodeTeams::Team)) }
8572
def map_file_to_relevant_owner(file)
86-
file_path = Pathname.new(file)
87-
team = T.let(nil, T.nilable(CodeTeams::Team))
88-
89-
if File.directory?(file)
90-
team = get_team_from_codeowners_file_within_directory(file_path)
91-
end
92-
93-
while team.nil? && file_path != RELATIVE_ROOT && file_path != ABSOLUTE_ROOT
94-
file_path = file_path.parent
95-
team = get_team_from_codeowners_file_within_directory(file_path)
96-
end
97-
98-
team
99-
end
100-
101-
sig { params(directory: Pathname).returns(T.nilable(CodeTeams::Team)) }
102-
def get_team_from_codeowners_file_within_directory(directory)
103-
potential_codeowners_file = directory.join(CODEOWNERS_DIRECTORY_FILE_NAME)
104-
105-
potential_codeowners_file_name = potential_codeowners_file.to_s
106-
107-
team = nil
108-
if @@directory_cache.key?(potential_codeowners_file_name)
109-
team = @@directory_cache[potential_codeowners_file_name]
110-
elsif potential_codeowners_file.exist?
111-
team = owner_for_codeowners_file(potential_codeowners_file)
112-
113-
@@directory_cache[potential_codeowners_file_name] = team
114-
else
115-
@@directory_cache[potential_codeowners_file_name] = nil
116-
end
117-
118-
return team
73+
FileOwner.for_file(file, @@directory_cache)
11974
end
12075
end
12176
end
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# frozen_string_literal: true
2+
3+
# typed: strict
4+
5+
module CodeOwnership
6+
module Private
7+
module OwnershipMappers
8+
class FileOwner
9+
extend T::Sig
10+
11+
sig { returns(String) }
12+
attr_reader :file_path
13+
sig { returns(T::Hash[String, T.nilable(CodeTeams::Team)]) }
14+
attr_reader :directory_cache
15+
16+
private_class_method :new
17+
18+
sig { params(file_path: String, directory_cache: T::Hash[String, T.nilable(CodeTeams::Team)]).void }
19+
def initialize(file_path, directory_cache)
20+
@file_path = file_path
21+
@directory_cache = directory_cache
22+
end
23+
24+
sig { params(file_path: String, directory_cache: T::Hash[String, T.nilable(CodeTeams::Team)]).returns(T.nilable(CodeTeams::Team)) }
25+
def self.for_file(file_path, directory_cache)
26+
new(file_path, directory_cache).owner
27+
end
28+
29+
sig { returns(T.nilable(CodeTeams::Team)) }
30+
def owner
31+
path_components_end_index.downto(0).each do |index|
32+
team = team_for_path_components(T.must(path_components[0..index]))
33+
return team if team
34+
end
35+
nil
36+
end
37+
38+
sig { params(codeowners_file: Pathname).returns(CodeTeams::Team) }
39+
def self.owner_for_codeowners_file(codeowners_file)
40+
raw_owner_value = File.foreach(codeowners_file).first.strip
41+
42+
Private.find_team!(
43+
raw_owner_value,
44+
codeowners_file.to_s
45+
)
46+
end
47+
48+
private
49+
50+
sig { params(path_components: T::Array[Pathname]).returns(T.nilable(CodeTeams::Team)) }
51+
def team_for_path_components(path_components)
52+
potential_relative_path_name = path_components.reduce(Pathname.new('')) do |built_path, path|
53+
built_path.join(path)
54+
end
55+
56+
potential_codeowners_file = potential_relative_path_name.join(DirectoryOwnership::CODEOWNERS_DIRECTORY_FILE_NAME)
57+
potential_codeowners_file_name = potential_codeowners_file.to_s
58+
59+
team = nil
60+
if directory_cache.key?(potential_codeowners_file_name)
61+
team = directory_cache[potential_codeowners_file_name]
62+
elsif potential_codeowners_file.exist?
63+
team = self.class.owner_for_codeowners_file(potential_codeowners_file)
64+
directory_cache[potential_codeowners_file_name] = team
65+
else
66+
directory_cache[potential_codeowners_file_name] = nil
67+
end
68+
69+
team
70+
end
71+
72+
sig { returns(Pathname) }
73+
def file_path_name
74+
T.let(Pathname.new(file_path), T.nilable(Pathname))
75+
@file_path_name ||= T.let(Pathname.new(file_path), T.nilable(Pathname))
76+
end
77+
78+
sig { returns(T::Boolean) }
79+
def file_is_dir?
80+
file_path_name.directory?
81+
end
82+
83+
sig { returns(Integer) }
84+
def path_components_end_index
85+
# include the directory itself if it is a directory, but not if it is a file
86+
if file_is_dir?
87+
path_components.length
88+
else
89+
path_components.length - 1
90+
end
91+
end
92+
93+
sig { returns(T::Array[Pathname]) }
94+
def path_components
95+
file_path_name.each_filename.to_a.map { |path| Pathname.new(path) }
96+
end
97+
end
98+
end
99+
end
100+
end

spec/lib/code_ownership/private/ownership_mappers/directory_ownership_spec.rb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
module CodeOwnership
2-
RSpec.describe Private::OwnershipMappers::JsPackageOwnership do
2+
RSpec.describe Private::OwnershipMappers::DirectoryOwnership do
33
describe 'CodeOwnershp.for_file' do
44
before do
55
write_configuration
@@ -22,9 +22,13 @@ module CodeOwnership
2222
expect(CodeOwnership.for_file('a/b/c/c_file.jsx').name).to eq 'Bar'
2323
end
2424

25+
26+
it 'looks for codeowner file within lower directory' do
27+
expect(CodeOwnership.for_file('a/b/c').name).to eq 'Bar'
28+
end
29+
2530
it 'looks for codeowner file within directory' do
2631
expect(CodeOwnership.for_file('a/b').name).to eq 'Bar'
27-
expect(CodeOwnership.for_file(Pathname.pwd.join('a/b').to_s).name).to eq 'Bar'
2832
end
2933
end
3034
end
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# frozen_string_literal: true
2+
3+
module CodeOwnership
4+
RSpec.describe Private::OwnershipMappers::FileOwner do
5+
describe 'CodeOwnershp.for_file' do
6+
subject { described_class.for_file(file_path, directory_cache) }
7+
let(:directory_cache) { {} }
8+
before do
9+
write_configuration
10+
11+
write_file('a/b/.codeowner', <<~CONTENTS)
12+
Bar
13+
CONTENTS
14+
write_file('z/y/x/x_file.jsx')
15+
write_file('a/b/c/c_file.jsx')
16+
write_file('a/b/b_file.jsx')
17+
write_file('a/b/c/d/e/f/g/h_file.jsx')
18+
write_file('config/teams/bar.yml', <<~CONTENTS)
19+
name: Bar
20+
CONTENTS
21+
end
22+
23+
context 'when file provided' do
24+
let(:file_path) { 'a/b/b_file.jsx' }
25+
26+
it 'can find the owner of files in team-owned directory' do
27+
expect(subject.name).to eq 'Bar'
28+
end
29+
30+
context 'when un-owned file' do
31+
let(:file_path) { 'z/y/x/x_file.jsx' }
32+
it 'returns nil' do
33+
expect(subject).to be_nil
34+
end
35+
end
36+
end
37+
38+
context 'when directory provided' do
39+
let(:file_path) { 'a/b' }
40+
41+
it 'can find the owner of files in team-owned directory' do
42+
expect(subject.name).to eq 'Bar'
43+
end
44+
45+
context 'when deeply nested directory' do
46+
let(:file_path) { 'a/b/c/d/e/f/g' }
47+
48+
it 'can find the owner of files in team-owned directory' do
49+
expect(subject.name).to eq 'Bar'
50+
end
51+
end
52+
53+
context 'when un-owned directory' do
54+
let(:file_path) { 'z/y' }
55+
it 'returns nil' do
56+
expect(subject).to be_nil
57+
end
58+
end
59+
end
60+
end
61+
end
62+
end

0 commit comments

Comments
 (0)