Skip to content

Commit 8913407

Browse files
author
Alex Evanczuk
authored
Fix bug with "files have unique owners" validation (#31)
* Add failing test * Fix failing test * Bump version * Fix non-determinism in output so CI passes
1 parent b4fb864 commit 8913407

File tree

6 files changed

+131
-3
lines changed

6 files changed

+131
-3
lines changed

Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PATH
22
remote: .
33
specs:
4-
code_ownership (1.29.3)
4+
code_ownership (1.30.0)
55
code_teams (~> 1.0)
66
packs
77
sorbet-runtime

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.29.3'
3+
spec.version = '1.30.0'
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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
require 'code_ownership/private/validations/files_have_owners'
1111
require 'code_ownership/private/validations/github_codeowners_up_to_date'
1212
require 'code_ownership/private/validations/files_have_unique_owners'
13+
require 'code_ownership/private/validations/no_overlapping_globs'
1314
require 'code_ownership/private/ownership_mappers/interface'
1415
require 'code_ownership/private/ownership_mappers/file_annotations'
1516
require 'code_ownership/private/ownership_mappers/team_globs'
@@ -39,6 +40,7 @@ def self.validate!(files:, autocorrect: true, stage_changes: true)
3940
Validations::FilesHaveOwners.new,
4041
Validations::FilesHaveUniqueOwners.new,
4142
Validations::GithubCodeownersUpToDate.new,
43+
Validations::NoOverlappingGlobs.new,
4244
]
4345

4446
errors = validators.flat_map do |validator|

lib/code_ownership/private/ownership_mappers/team_globs.rb

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,58 @@ def map_files_to_owners(files) # rubocop:disable Lint/UnusedMethodArgument
3131
end
3232
end
3333

34+
class MappingContext < T::Struct
35+
const :glob, String
36+
const :team, CodeTeams::Team
37+
end
38+
39+
class GlobOverlap < T::Struct
40+
extend T::Sig
41+
42+
const :mapping_contexts, T::Array[MappingContext]
43+
44+
sig { returns(String) }
45+
def description
46+
# These are sorted only to prevent non-determinism in output between local and CI environments.
47+
sorted_contexts = mapping_contexts.sort_by{|context| context.team.config_yml.to_s }
48+
description_args = sorted_contexts.map do |context|
49+
"`#{context.glob}` (from `#{context.team.config_yml}`)"
50+
end
51+
52+
description_args.join(', ')
53+
end
54+
end
55+
56+
sig do
57+
returns(T::Array[GlobOverlap])
58+
end
59+
def find_overlapping_globs
60+
mapped_files = T.let({}, T::Hash[String, T::Array[MappingContext]])
61+
CodeTeams.all.each_with_object({}) do |team, map| # rubocop:disable Style/ClassVars
62+
TeamPlugins::Ownership.for(team).owned_globs.each do |glob|
63+
Dir.glob(glob).each do |filename|
64+
mapped_files[filename] ||= []
65+
T.must(mapped_files[filename]) << MappingContext.new(glob: glob, team: team)
66+
end
67+
end
68+
end
69+
70+
overlaps = T.let([], T::Array[GlobOverlap])
71+
mapped_files.each do |filename, mapping_contexts|
72+
if mapping_contexts.count > 1
73+
overlaps << GlobOverlap.new(mapping_contexts: mapping_contexts)
74+
end
75+
end
76+
77+
deduplicated_overlaps = overlaps.uniq do |glob_overlap|
78+
glob_overlap.mapping_contexts.map do |context|
79+
[context.glob, context.team.name]
80+
end
81+
end
82+
83+
deduplicated_overlaps
84+
end
85+
3486
sig do
3587
override.params(file: String).
3688
returns(T.nilable(::CodeTeams::Team))
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# typed: strict
2+
3+
module CodeOwnership
4+
module Private
5+
module Validations
6+
class NoOverlappingGlobs
7+
extend T::Sig
8+
extend T::Helpers
9+
include Interface
10+
11+
sig { override.params(files: T::Array[String], autocorrect: T::Boolean, stage_changes: T::Boolean).returns(T::Array[String]) }
12+
def validation_errors(files:, autocorrect: true, stage_changes: true)
13+
overlapping_globs = OwnershipMappers::TeamGlobs.new.find_overlapping_globs
14+
15+
errors = T.let([], T::Array[String])
16+
17+
if overlapping_globs.any?
18+
errors << <<~MSG
19+
`owned_globs` cannot overlap between teams. The following globs overlap:
20+
21+
#{overlapping_globs.map { |overlap| "- #{overlap.description}"}.join("\n")}
22+
MSG
23+
end
24+
25+
errors
26+
end
27+
end
28+
end
29+
end
30+
end

spec/lib/code_ownership_spec.rb

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -662,6 +662,51 @@
662662
end
663663
end
664664
end
665+
666+
describe 'no overlapping globs validation' do
667+
context 'two teams own the same exact glob' do
668+
before do
669+
write_file('config/code_ownership.yml', <<~YML)
670+
owned_globs:
671+
- '{app,components,config,frontend,lib,packs,spec}/**/*.{rb,rake,js,jsx,ts,tsx}'
672+
YML
673+
674+
write_file('packs/my_pack/owned_file.rb')
675+
write_file('frontend/javascripts/blah/my_file.rb')
676+
write_file('frontend/javascripts/blah/subdir/my_file.rb')
677+
678+
write_file('config/teams/bar.yml', <<~CONTENTS)
679+
name: Bar
680+
owned_globs:
681+
- packs/**/**
682+
- frontend/javascripts/blah/subdir/my_file.rb
683+
CONTENTS
684+
685+
write_file('config/teams/foo.yml', <<~CONTENTS)
686+
name: Foo
687+
owned_globs:
688+
- packs/**/**
689+
- frontend/javascripts/blah/**/**
690+
CONTENTS
691+
end
692+
693+
it 'lets the user know that `owned_globs` can not overlap' do
694+
expect { CodeOwnership.validate! }.to raise_error do |e|
695+
expect(e).to be_a CodeOwnership::InvalidCodeOwnershipConfigurationError
696+
puts e.message
697+
expect(e.message).to eq <<~EXPECTED.chomp
698+
`owned_globs` cannot overlap between teams. The following globs overlap:
699+
700+
- `packs/**/**` (from `config/teams/bar.yml`), `packs/**/**` (from `config/teams/foo.yml`)
701+
- `frontend/javascripts/blah/subdir/my_file.rb` (from `config/teams/bar.yml`), `frontend/javascripts/blah/**/**` (from `config/teams/foo.yml`)
702+
703+
See https://github.com/rubyatscale/code_ownership#README.md for more details
704+
EXPECTED
705+
end
706+
end
707+
end
708+
end
709+
665710
end
666711

667712
describe '.for_file' do
@@ -983,5 +1028,4 @@
9831028
end
9841029
end
9851030
end
986-
9871031
end

0 commit comments

Comments
 (0)