11use file_owner_finder:: FileOwnerFinder ;
22use mapper:: { OwnerMatcher , Source , TeamName } ;
33use std:: {
4+ error:: Error ,
45 fmt:: { self , Display } ,
56 path:: Path ,
67 sync:: Arc ,
@@ -32,6 +33,21 @@ pub struct FileOwner {
3233 pub sources : Vec < Source > ,
3334}
3435
36+ #[ derive( Debug , Default , Clone , PartialEq ) ]
37+ pub struct TeamOwnership {
38+ pub heading : String ,
39+ pub globs : Vec < String > ,
40+ }
41+
42+ impl TeamOwnership {
43+ fn new ( heading : String ) -> Self {
44+ Self {
45+ heading,
46+ ..Default :: default ( )
47+ }
48+ }
49+ }
50+
3551impl Display for FileOwner {
3652 fn fmt ( & self , f : & mut fmt:: Formatter < ' _ > ) -> fmt:: Result {
3753 let sources = self
@@ -121,6 +137,15 @@ impl Ownership {
121137 . collect ( ) )
122138 }
123139
140+ #[ instrument( level = "debug" , skip_all) ]
141+ pub fn for_team ( & self , team_name : & str ) -> Result < Vec < TeamOwnership > , Box < dyn Error > > {
142+ info ! ( "getting team ownership for {}" , team_name) ;
143+ let team = self . project . get_team ( team_name) . ok_or ( "Team not found" ) ?;
144+ let codeowners_file = self . project . get_codeowners_file ( ) ?;
145+
146+ parse_for_team ( team. github_team , & codeowners_file)
147+ }
148+
124149 #[ instrument( level = "debug" , skip_all) ]
125150 pub fn generate_file ( & self ) -> String {
126151 info ! ( "generating codeowners file" ) ;
@@ -141,12 +166,49 @@ impl Ownership {
141166 }
142167}
143168
169+ fn parse_for_team ( team_name : String , codeowners_file : & str ) -> Result < Vec < TeamOwnership > , Box < dyn Error > > {
170+ let mut output = vec ! [ ] ;
171+ let mut current_section: Option < TeamOwnership > = None ;
172+ let input: String = codeowners_file. replace ( & FileGenerator :: disclaimer ( ) . join ( "\n " ) , "" ) ;
173+ let error_message = "CODEOWNERS out of date. Run `codeowners generate` to update the CODEOWNERS file" ;
174+
175+ for line in input. trim_start ( ) . lines ( ) {
176+ match line {
177+ comment if comment. starts_with ( "#" ) => {
178+ if let Some ( section) = current_section. take ( ) {
179+ output. push ( section) ;
180+ }
181+ current_section = Some ( TeamOwnership :: new ( comment. to_string ( ) ) ) ;
182+ }
183+ "" => {
184+ if let Some ( section) = current_section. take ( ) {
185+ output. push ( section) ;
186+ }
187+ }
188+ team_line if team_line. ends_with ( & team_name) => {
189+ let section = current_section. as_mut ( ) . ok_or ( error_message) ?;
190+
191+ let glob = line. split_once ( ' ' ) . ok_or ( error_message) ?. 0 . to_string ( ) ;
192+ section. globs . push ( glob) ;
193+ }
194+ _ => { }
195+ }
196+ }
197+
198+ if let Some ( cs) = current_section {
199+ output. push ( cs. clone ( ) ) ;
200+ }
201+
202+ Ok ( output)
203+ }
204+
144205#[ cfg( test) ]
145206mod tests {
146- use crate :: common_test:: tests:: build_ownership_with_all_mappers;
207+ use super :: * ;
208+ use crate :: common_test:: tests:: { build_ownership_with_all_mappers, vecs_match} ;
147209
148210 #[ test]
149- fn test_for_file_owner ( ) -> Result < ( ) , Box < dyn std :: error :: Error > > {
211+ fn test_for_file_owner ( ) -> Result < ( ) , Box < dyn Error > > {
150212 let ownership = build_ownership_with_all_mappers ( ) ?;
151213 let file_owners = ownership. for_file ( "app/consumers/directory_owned.rb" ) . unwrap ( ) ;
152214 assert_eq ! ( file_owners. len( ) , 1 ) ;
@@ -156,10 +218,191 @@ mod tests {
156218 }
157219
158220 #[ test]
159- fn test_for_file_no_owner ( ) -> Result < ( ) , Box < dyn std :: error :: Error > > {
221+ fn test_for_file_no_owner ( ) -> Result < ( ) , Box < dyn Error > > {
160222 let ownership = build_ownership_with_all_mappers ( ) ?;
161223 let file_owners = ownership. for_file ( "app/madeup/foo.rb" ) . unwrap ( ) ;
162224 assert_eq ! ( file_owners. len( ) , 0 ) ;
163225 Ok ( ( ) )
164226 }
227+
228+ #[ test]
229+ fn test_for_team ( ) -> Result < ( ) , Box < dyn Error > > {
230+ let ownership = build_ownership_with_all_mappers ( ) ?;
231+ let team_ownership = ownership. for_team ( "Bar" ) ;
232+ assert ! ( team_ownership. is_ok( ) ) ;
233+ Ok ( ( ) )
234+ }
235+
236+ #[ test]
237+ fn test_for_team_not_found ( ) -> Result < ( ) , Box < dyn Error > > {
238+ let ownership = build_ownership_with_all_mappers ( ) ?;
239+ let team_ownership = ownership. for_team ( "Nope" ) ;
240+ assert ! ( team_ownership. is_err( ) , "Team not found" ) ;
241+ Ok ( ( ) )
242+ }
243+
244+ #[ test]
245+ fn test_parse_for_team_trims_header ( ) -> Result < ( ) , Box < dyn Error > > {
246+ let codeownership_file = r#"
247+ # STOP! - DO NOT EDIT THIS FILE MANUALLY
248+ # This file was automatically generated by "bin/codeownership validate".
249+ #
250+ # CODEOWNERS is used for GitHub to suggest code/file owners to various GitHub
251+ # teams. This is useful when developers create Pull Requests since the
252+ # code/file owner is notified. Reference GitHub docs for more details:
253+ # https://help.github.com/en/articles/about-code-owners
254+
255+
256+ "# ;
257+
258+ let team_ownership = parse_for_team ( "@Bar" . to_string ( ) , codeownership_file) ?;
259+ assert ! ( team_ownership. is_empty( ) ) ;
260+ Ok ( ( ) )
261+ }
262+
263+ #[ test]
264+ fn test_parse_for_team_includes_owned_globs ( ) -> Result < ( ) , Box < dyn Error > > {
265+ let codeownership_file = r#"
266+ # First Section
267+ /path/to/owned @Foo
268+ /path/to/not/owned @Bar
269+
270+ # Last Section
271+ /another/owned/path @Foo
272+ "# ;
273+
274+ let team_ownership = parse_for_team ( "@Foo" . to_string ( ) , codeownership_file) ?;
275+ vecs_match (
276+ & team_ownership,
277+ & vec ! [
278+ TeamOwnership {
279+ heading: "# First Section" . to_string( ) ,
280+ globs: vec![ "/path/to/owned" . to_string( ) ] ,
281+ } ,
282+ TeamOwnership {
283+ heading: "# Last Section" . to_string( ) ,
284+ globs: vec![ "/another/owned/path" . to_string( ) ] ,
285+ } ,
286+ ] ,
287+ ) ;
288+ Ok ( ( ) )
289+ }
290+
291+ #[ test]
292+ fn test_parse_for_team_with_partial_team_match ( ) -> Result < ( ) , Box < dyn Error > > {
293+ let codeownership_file = r#"
294+ # First Section
295+ /path/to/owned @Foo
296+ /path/to/not/owned @FooBar
297+ "# ;
298+
299+ let team_ownership = parse_for_team ( "@Foo" . to_string ( ) , codeownership_file) ?;
300+ vecs_match (
301+ & team_ownership,
302+ & vec ! [ TeamOwnership {
303+ heading: "# First Section" . to_string( ) ,
304+ globs: vec![ "/path/to/owned" . to_string( ) ] ,
305+ } ] ,
306+ ) ;
307+ Ok ( ( ) )
308+ }
309+
310+ #[ test]
311+ fn test_parse_for_team_with_trailing_newlines ( ) -> Result < ( ) , Box < dyn Error > > {
312+ let codeownership_file = r#"
313+ # First Section
314+ /path/to/owned @Foo
315+
316+ # Last Section
317+ /another/owned/path @Foo
318+
319+
320+
321+ "# ;
322+
323+ let team_ownership = parse_for_team ( "@Foo" . to_string ( ) , codeownership_file) ?;
324+ vecs_match (
325+ & team_ownership,
326+ & vec ! [
327+ TeamOwnership {
328+ heading: "# First Section" . to_string( ) ,
329+ globs: vec![ "/path/to/owned" . to_string( ) ] ,
330+ } ,
331+ TeamOwnership {
332+ heading: "# Last Section" . to_string( ) ,
333+ globs: vec![ "/another/owned/path" . to_string( ) ] ,
334+ } ,
335+ ] ,
336+ ) ;
337+ Ok ( ( ) )
338+ }
339+
340+ #[ test]
341+ fn test_parse_for_team_without_trailing_newline ( ) -> Result < ( ) , Box < dyn Error > > {
342+ let codeownership_file = r#"
343+ # First Section
344+ /path/to/owned @Foo"# ;
345+
346+ let team_ownership = parse_for_team ( "@Foo" . to_string ( ) , codeownership_file) ?;
347+ vecs_match (
348+ & team_ownership,
349+ & vec ! [ TeamOwnership {
350+ heading: "# First Section" . to_string( ) ,
351+ globs: vec![ "/path/to/owned" . to_string( ) ] ,
352+ } ] ,
353+ ) ;
354+ Ok ( ( ) )
355+ }
356+
357+ #[ test]
358+ fn test_parse_for_team_with_missing_section_header ( ) -> Result < ( ) , Box < dyn Error > > {
359+ let codeownership_file = r#"
360+ # First Section
361+ /path/to/owned @Foo
362+
363+ /another/owned/path @Foo
364+ "# ;
365+
366+ let team_ownership = parse_for_team ( "@Foo" . to_string ( ) , codeownership_file) ;
367+ assert ! ( team_ownership
368+ . is_err_and( |e| e. to_string( ) == "CODEOWNERS out of date. Run `codeowners generate` to update the CODEOWNERS file" ) ) ;
369+ Ok ( ( ) )
370+ }
371+
372+ #[ test]
373+ fn test_parse_for_team_with_malformed_team_line ( ) -> Result < ( ) , Box < dyn Error > > {
374+ let codeownership_file = r#"
375+ # First Section
376+ @Foo
377+ "# ;
378+
379+ let team_ownership = parse_for_team ( "@Foo" . to_string ( ) , codeownership_file) ;
380+ assert ! ( team_ownership
381+ . is_err_and( |e| e. to_string( ) == "CODEOWNERS out of date. Run `codeowners generate` to update the CODEOWNERS file" ) ) ;
382+ Ok ( ( ) )
383+ }
384+
385+ #[ test]
386+ fn test_parse_for_team_with_invalid_file ( ) -> Result < ( ) , Box < dyn Error > > {
387+ let codeownership_file = r#"
388+ # First Section
389+ # Second Section
390+ path/to/owned @Foo
391+ "# ;
392+ let team_ownership = parse_for_team ( "@Foo" . to_string ( ) , codeownership_file) ?;
393+ vecs_match (
394+ & team_ownership,
395+ & vec ! [
396+ TeamOwnership {
397+ heading: "# First Section" . to_string( ) ,
398+ globs: vec![ ] ,
399+ } ,
400+ TeamOwnership {
401+ heading: "# Second Section" . to_string( ) ,
402+ globs: vec![ "path/to/owned" . to_string( ) ] ,
403+ } ,
404+ ] ,
405+ ) ;
406+ Ok ( ( ) )
407+ }
165408}
0 commit comments