@@ -2,6 +2,7 @@ use crate::project::{Project, ProjectFile};
22use core:: fmt;
33use std:: collections:: HashSet ;
44use std:: fmt:: Display ;
5+ use std:: path:: Path ;
56use std:: path:: PathBuf ;
67use std:: sync:: Arc ;
78
@@ -15,7 +16,7 @@ use tracing::instrument;
1516use super :: file_generator:: FileGenerator ;
1617use super :: file_owner_finder:: FileOwnerFinder ;
1718use super :: file_owner_finder:: Owner ;
18- use super :: mapper:: { Mapper , OwnerMatcher , TeamName } ;
19+ use super :: mapper:: { Mapper , OwnerMatcher , Source , TeamName } ;
1920
2021pub struct Validator {
2122 pub project : Arc < Project > ,
@@ -27,7 +28,7 @@ pub struct Validator {
2728enum Error {
2829 InvalidTeam { name : String , path : PathBuf } ,
2930 FileWithoutOwner { path : PathBuf } ,
30- FileWithMultipleOwners { path : PathBuf , owners : Vec < Owner > } ,
31+ MultipleTeamYmls { path : PathBuf , owners : Vec < Owner > } ,
3132 CodeownershipFileIsStale ,
3233}
3334
@@ -113,11 +114,8 @@ impl Validator {
113114
114115 if owners. is_empty ( ) {
115116 validation_errors. push ( Error :: FileWithoutOwner { path : relative_path } )
116- } else if owners. len ( ) > 1 {
117- validation_errors. push ( Error :: FileWithMultipleOwners {
118- path : relative_path,
119- owners,
120- } )
117+ } else if let Some ( multiple_team_file_owners_error) = multiple_team_file_owners ( & owners, & relative_path) {
118+ validation_errors. push ( multiple_team_file_owners_error) ;
121119 }
122120 }
123121
@@ -162,7 +160,7 @@ impl Error {
162160 pub fn category ( & self ) -> String {
163161 match self {
164162 Error :: FileWithoutOwner { path : _ } => "Some files are missing ownership" . to_owned ( ) ,
165- Error :: FileWithMultipleOwners { path : _, owners : _ } => "Code ownership should only be defined for each file in one way. The following files have declared ownership in multiple ways " . to_owned ( ) ,
163+ Error :: MultipleTeamYmls { path : _, owners : _ } => "Team yml file globs should not overlap " . to_owned ( ) ,
166164 Error :: CodeownershipFileIsStale => {
167165 "CODEOWNERS out of date. Run `codeowners generate` to update the CODEOWNERS file" . to_owned ( )
168166 }
@@ -173,7 +171,7 @@ impl Error {
173171 pub fn messages ( & self ) -> Vec < String > {
174172 match self {
175173 Error :: FileWithoutOwner { path } => vec ! [ format!( "- {}" , path. to_string_lossy( ) ) ] ,
176- Error :: FileWithMultipleOwners { path, owners } => {
174+ Error :: MultipleTeamYmls { path, owners } => {
177175 let path_display = path. to_string_lossy ( ) ;
178176 let mut messages = vec ! [ format!( "\n {path_display}" ) ] ;
179177
@@ -193,6 +191,29 @@ impl Error {
193191 }
194192}
195193
194+ fn multiple_team_file_owners ( owners : & [ Owner ] , relative_path : & Path ) -> Option < Error > {
195+ if owners. len ( ) <= 1 {
196+ return None ;
197+ }
198+ let team_file_owners: Vec < Owner > = owners
199+ . iter ( )
200+ . filter ( |owner| owner. sources . iter ( ) . any ( |source| matches ! ( source, Source :: TeamYml ) ) )
201+ . map ( |owner| Owner {
202+ sources : owner. sources . clone ( ) ,
203+ team_name : owner. team_name . clone ( ) ,
204+ } )
205+ . collect ( ) ;
206+
207+ if team_file_owners. len ( ) > 1 {
208+ Some ( Error :: MultipleTeamYmls {
209+ path : relative_path. to_path_buf ( ) ,
210+ owners : team_file_owners,
211+ } )
212+ } else {
213+ None
214+ }
215+ }
216+
196217impl Display for Errors {
197218 fn fmt ( & self , f : & mut fmt:: Formatter ) -> fmt:: Result {
198219 let grouped_errors = self . 0 . iter ( ) . into_group_map_by ( |error| error. category ( ) ) ;
@@ -216,3 +237,83 @@ impl Display for Errors {
216237}
217238
218239impl Context for Errors { }
240+
241+ #[ cfg( test) ]
242+ mod tests {
243+ use super :: * ;
244+
245+ fn owner_with_sources ( team : & str , sources : Vec < Source > ) -> Owner {
246+ Owner {
247+ team_name : team. to_string ( ) ,
248+ sources,
249+ }
250+ }
251+
252+ #[ test]
253+ fn multiple_team_file_owners_with_no_owners_returns_none ( ) {
254+ let owners: Vec < Owner > = vec ! [ ] ;
255+ let path = PathBuf :: from ( "app/models/user.rb" ) ;
256+
257+ let result = multiple_team_file_owners ( & owners, & path) ;
258+ assert ! ( result. is_none( ) ) ;
259+ }
260+
261+ #[ test]
262+ fn multiple_team_file_owners_with_single_owner_returns_none ( ) {
263+ let owners = vec ! [ owner_with_sources( "Foo" , vec![ Source :: TeamYml ] ) ] ;
264+ let path = PathBuf :: from ( "app/models/user.rb" ) ;
265+
266+ let result = multiple_team_file_owners ( & owners, & path) ;
267+ assert ! ( result. is_none( ) ) ;
268+ }
269+
270+ #[ test]
271+ fn multiple_team_file_owners_with_multiple_non_teamfile_owners_returns_none ( ) {
272+ let owners = vec ! [
273+ owner_with_sources( "Foo" , vec![ Source :: Directory ( "app" . to_string( ) ) ] ) ,
274+ owner_with_sources( "Bar" , vec![ Source :: TeamGlob ( "packs/bar/**" . to_string( ) ) ] ) ,
275+ ] ;
276+ let path = PathBuf :: from ( "app/models/user.rb" ) ;
277+
278+ let result = multiple_team_file_owners ( & owners, & path) ;
279+ assert ! ( result. is_none( ) ) ;
280+ }
281+
282+ #[ test]
283+ fn multiple_team_file_owners_with_one_teamfile_owner_returns_none ( ) {
284+ let owners = vec ! [
285+ owner_with_sources( "Foo" , vec![ Source :: TeamYml ] ) ,
286+ owner_with_sources( "Bar" , vec![ Source :: Directory ( "app/services" . to_string( ) ) ] ) ,
287+ ] ;
288+ let path = PathBuf :: from ( "app/services/service.rb" ) ;
289+
290+ let result = multiple_team_file_owners ( & owners, & path) ;
291+ assert ! ( result. is_none( ) ) ;
292+ }
293+
294+ #[ test]
295+ fn multiple_team_file_owners_with_two_teamfile_owners_returns_error ( ) {
296+ let owners = vec ! [
297+ owner_with_sources( "Foo" , vec![ Source :: TeamYml ] ) ,
298+ owner_with_sources( "Bar" , vec![ Source :: TeamYml ] ) ,
299+ ] ;
300+ let path = PathBuf :: from ( "packs/payroll/services/runner.rb" ) ;
301+
302+ let result = multiple_team_file_owners ( & owners, & path) ;
303+ match result {
304+ Some ( Error :: MultipleTeamYmls {
305+ path : p,
306+ owners : conflicting,
307+ } ) => {
308+ assert_eq ! ( p, path) ;
309+ assert_eq ! ( conflicting. len( ) , 2 ) ;
310+ let mut names: Vec < & str > = conflicting. iter ( ) . map ( |o| o. team_name . as_str ( ) ) . collect ( ) ;
311+ names. sort_unstable ( ) ;
312+ assert_eq ! ( names, vec![ "Bar" , "Foo" ] ) ;
313+ // Ensure sources are preserved as TeamFile for both
314+ assert ! ( conflicting. iter( ) . all( |o| o. sources. iter( ) . any( |s| matches!( s, Source :: TeamYml ) ) ) ) ;
315+ }
316+ _ => panic ! ( "Expected MultipleTeamYmls error" ) ,
317+ }
318+ }
319+ }
0 commit comments