@@ -4,6 +4,7 @@ use crate::{
44} ;
55use fast_glob:: glob_match;
66use memoize:: memoize;
7+ use regex:: Regex ;
78use std:: {
89 collections:: HashMap ,
910 error:: Error ,
@@ -12,6 +13,8 @@ use std::{
1213 path:: { Path , PathBuf } ,
1314} ;
1415
16+ use super :: file_generator:: compare_lines;
17+
1518pub struct Parser {
1619 pub project_root : PathBuf ,
1720 pub codeowners_file_path : PathBuf ,
@@ -54,7 +57,6 @@ impl Parser {
5457
5558#[ memoize]
5659fn teams_by_github_team_name ( team_file_glob : Vec < String > ) -> HashMap < String , Team > {
57- dbg ! ( "in teams_by_github_team_name" ) ;
5860 let mut teams = HashMap :: new ( ) ;
5961 for glob in team_file_glob {
6062 let paths = glob:: glob ( & glob) . expect ( "Failed to read glob pattern" ) . filter_map ( Result :: ok) ;
@@ -76,8 +78,6 @@ fn teams_by_github_team_name(team_file_glob: Vec<String>) -> HashMap<String, Tea
7678
7779#[ memoize]
7880fn build_codeowners_lines_in_priority ( codeowners_file_path : String ) -> Vec < String > {
79- dbg ! ( "in build_codeowners_lines_in_priority" ) ;
80- dbg ! ( & codeowners_file_path) ;
8181 let codeowners_file = match fs:: read_to_string ( codeowners_file_path) {
8282 Ok ( codeowners_file) => codeowners_file,
8383 Err ( e) => {
@@ -89,13 +89,67 @@ fn build_codeowners_lines_in_priority(codeowners_file_path: String) -> Vec<Strin
8989 stripped_lines_by_priority ( & codeowners_file)
9090}
9191
92+ #[ derive( Debug , Clone , PartialEq , Eq ) ]
93+ struct Section {
94+ heading : String ,
95+ lines : Vec < String > ,
96+ }
97+
98+ impl Section {
99+ fn new ( heading : String , lines : Vec < String > ) -> Self {
100+ let mut sorted_lines = lines. clone ( ) ;
101+ sorted_lines. sort_by ( compare_lines) ;
102+ Self {
103+ heading,
104+ lines : sorted_lines,
105+ }
106+ }
107+ }
108+
109+ fn codeowner_sections ( codeowners_file : & str ) -> Result < Vec < Section > , Box < dyn Error > > {
110+ let un_ignore = Regex :: new ( r"^# \/" ) ?;
111+ let mut iter = codeowners_file. lines ( ) . peekable ( ) ;
112+ let mut sections = Vec :: new ( ) ;
113+ let mut current_section = None ;
114+ let mut current_lines = Vec :: new ( ) ;
115+
116+ while let Some ( line) = iter. next ( ) {
117+ let line = un_ignore. replace ( line, "/" ) . to_string ( ) ;
118+ if line. is_empty ( ) {
119+ continue ;
120+ }
121+
122+ if line. starts_with ( '#' ) {
123+ if iter
124+ . peek ( )
125+ . map ( |next| next. starts_with ( '/' ) || next. starts_with ( "# /" ) )
126+ . unwrap_or ( false )
127+ {
128+ if let Some ( section_name) = current_section. take ( ) {
129+ sections. push ( Section :: new ( section_name, std:: mem:: take ( & mut current_lines) ) ) ;
130+ }
131+ current_section = Some ( line) ;
132+ }
133+ } else {
134+ current_lines. push ( line) ;
135+ }
136+ }
137+
138+ if let Some ( section_name) = current_section {
139+ sections. push ( Section :: new ( section_name, current_lines) ) ;
140+ }
141+
142+ Ok ( sections)
143+ }
144+
92145fn stripped_lines_by_priority ( codeowners_file : & str ) -> Vec < String > {
93- codeowners_file
94- . lines ( )
95- . filter ( |line| !line. trim ( ) . is_empty ( ) && !line. trim ( ) . starts_with ( "#" ) )
96- . map ( |line| line. trim ( ) . to_string ( ) )
97- . rev ( )
98- . collect ( )
146+ let mut lines = Vec :: new ( ) ;
147+ let sections = codeowner_sections ( codeowners_file) . unwrap_or_default ( ) ;
148+ for section in sections {
149+ lines. extend ( section. lines ) ;
150+ }
151+ lines. reverse ( ) ;
152+ lines
99153}
100154
101155pub fn parse_for_team ( team_name : String , codeowners_file : & str ) -> Result < Vec < TeamOwnership > , Box < dyn Error > > {
@@ -136,6 +190,7 @@ pub fn parse_for_team(team_name: String, codeowners_file: &str) -> Result<Vec<Te
136190
137191#[ cfg( test) ]
138192mod tests {
193+
139194 use crate :: common_test:: tests:: vecs_match;
140195
141196 use super :: * ;
@@ -336,4 +391,90 @@ mod tests {
336391 assert_eq ! ( stripped_lines, vec![ "/another/path/to/owned @Bar" , "/path/to/owned @Foo" ] ) ;
337392 Ok ( ( ) )
338393 }
394+
395+ #[ test]
396+ fn test_stripped_lines_by_priority_with_ignored_teams ( ) -> Result < ( ) , Box < dyn Error > > {
397+ let codeownership_file = indoc ! { "
398+ # STOP! - DO NOT EDIT THIS FILE MANUALLY
399+ # This file was automatically generated by \" bin/codeownership validate\" .
400+ #
401+ # CODEOWNERS is used for GitHub to suggest code/file owners to various GitHub
402+ # teams. This is useful when developers create Pull Requests since the
403+ # code/file owner is notified. Reference GitHub docs for more details:
404+ # https://help.github.com/en/articles/about-code-owners
405+
406+
407+ # Annotations at the top of file
408+ # /app/assets/config/manifest.js @Prefix/team-foo
409+ # /config/application.rb @Prefix/team-bar
410+ /config/i18n-tasks.yml.erb @Prefix/language-team
411+
412+ # Team-specific owned globs
413+ # /.github/workflows/pull-translations.yml @Prefix/infra
414+ # /.github/workflows/push-sources.yml @Prefix/infra
415+ # /Dockerfile @Prefix/docker-team
416+ # /components/create.rb @Prefix/component-team
417+ /.codeclimate.yml @Prefix/climate-team
418+ /.vscode/extensions/z/**/* @Prefix/zteam
419+ /bin/brakeman @Prefix/psecurity
420+ /config/brakeman.ignore @Prefix/security
421+ " } ;
422+
423+ // build up each sections lines
424+ // resort the lines without the '#'
425+ // re-assemble the sections
426+ // reverse sort
427+ let codeowner_sections = codeowner_sections ( codeownership_file) ?;
428+ assert_eq ! (
429+ codeowner_sections,
430+ vec![
431+ Section {
432+ heading: "# Annotations at the top of file" . to_string( ) ,
433+ lines: vec![
434+ "/app/assets/config/manifest.js @Prefix/team-foo" . to_string( ) ,
435+ "/config/application.rb @Prefix/team-bar" . to_string( ) ,
436+ "/config/i18n-tasks.yml.erb @Prefix/language-team" . to_string( )
437+ ]
438+ } ,
439+ Section {
440+ heading: "# Team-specific owned globs" . to_string( ) ,
441+ lines: vec![
442+ "/.codeclimate.yml @Prefix/climate-team" . to_string( ) ,
443+ "/.github/workflows/pull-translations.yml @Prefix/infra" . to_string( ) ,
444+ "/.github/workflows/push-sources.yml @Prefix/infra" . to_string( ) ,
445+ "/.vscode/extensions/z/**/* @Prefix/zteam" . to_string( ) ,
446+ "/Dockerfile @Prefix/docker-team" . to_string( ) ,
447+ "/bin/brakeman @Prefix/psecurity" . to_string( ) ,
448+ "/components/create.rb @Prefix/component-team" . to_string( ) ,
449+ "/config/brakeman.ignore @Prefix/security" . to_string( )
450+ ]
451+ } ,
452+ ]
453+ ) ;
454+ let stripped_lines = stripped_lines_by_priority ( codeownership_file) ;
455+ assert_eq ! (
456+ stripped_lines,
457+ vec![
458+ "/config/brakeman.ignore @Prefix/security" ,
459+ "/components/create.rb @Prefix/component-team" ,
460+ "/bin/brakeman @Prefix/psecurity" ,
461+ "/Dockerfile @Prefix/docker-team" ,
462+ "/.vscode/extensions/z/**/* @Prefix/zteam" ,
463+ "/.github/workflows/push-sources.yml @Prefix/infra" ,
464+ "/.github/workflows/pull-translations.yml @Prefix/infra" ,
465+ "/.codeclimate.yml @Prefix/climate-team" ,
466+ "/config/i18n-tasks.yml.erb @Prefix/language-team" ,
467+ "/config/application.rb @Prefix/team-bar" ,
468+ "/app/assets/config/manifest.js @Prefix/team-foo"
469+ ]
470+ ) ;
471+ Ok ( ( ) )
472+ }
473+
474+ #[ test]
475+ fn test_unignore_regex ( ) -> Result < ( ) , Box < dyn Error > > {
476+ let un_ignore = Regex :: new ( r"^# \/" ) ?;
477+ assert_eq ! ( un_ignore. replace( "# /path/to/owned" , "/" ) , "/path/to/owned" ) ;
478+ Ok ( ( ) )
479+ }
339480}
0 commit comments