@@ -207,24 +207,14 @@ impl GitIgnoreMatcher {
207207 names : & [ String ] ,
208208 directory_flags : & [ u8 ] ,
209209 ) -> Vec < u8 > {
210- let relative_parent = normalize_relative_path ( relative_parent) ;
211- let separator = usize:: from ( !relative_parent. is_empty ( ) ) ;
212- let name_capacity = names. iter ( ) . map ( String :: len) . max ( ) . unwrap_or ( 0 ) ;
213- let mut relative_path =
214- String :: with_capacity ( relative_parent. len ( ) + separator + name_capacity) ;
215- let mut ignored = Vec :: with_capacity ( names. len ( ) ) ;
216-
217- for ( name, is_directory) in names. iter ( ) . zip ( directory_flags) {
218- relative_path. clear ( ) ;
219- if !relative_parent. is_empty ( ) {
220- relative_path. push_str ( relative_parent) ;
221- relative_path. push ( '/' ) ;
222- }
223- relative_path. push_str ( name) ;
224- ignored. push ( u8:: from (
225- self . is_ignored ( & relative_path, * is_directory != 0 ) ,
226- ) ) ;
227- }
210+ debug_assert_eq ! ( names. len( ) , directory_flags. len( ) ) ;
211+ let mut ignored = vec ! [ 0 ; names. len( ) ] ;
212+ self . match_batch (
213+ relative_parent,
214+ names,
215+ |index| directory_flags[ index] != 0 ,
216+ & mut ignored,
217+ ) ;
228218
229219 ignored
230220 }
@@ -257,26 +247,86 @@ impl GitIgnoreMatcher {
257247 ) -> u32 {
258248 debug_assert ! ( names. len( ) <= u32 :: BITS as usize ) ;
259249
250+ let mut ignored = [ 0 ; 32 ] ;
251+ self . match_batch (
252+ relative_parent,
253+ names,
254+ |index| directory_mask & ( 1 << index) != 0 ,
255+ & mut ignored[ ..names. len ( ) ] ,
256+ ) ;
257+ ignored[ ..names. len ( ) ]
258+ . iter ( )
259+ . enumerate ( )
260+ . fold ( 0 , |mask, ( index, ignored) | {
261+ mask | ( u32:: from ( * ignored) << index)
262+ } )
263+ }
264+
265+ fn match_batch (
266+ & mut self ,
267+ relative_parent : & str ,
268+ names : & [ String ] ,
269+ is_directory : impl Fn ( usize ) -> bool ,
270+ ignored : & mut [ u8 ] ,
271+ ) {
260272 let relative_parent = normalize_relative_path ( relative_parent) ;
261- let separator = usize:: from ( !relative_parent. is_empty ( ) ) ;
262- let name_capacity = names. iter ( ) . map ( String :: len) . max ( ) . unwrap_or ( 0 ) ;
263- let mut relative_path =
264- String :: with_capacity ( relative_parent. len ( ) + separator + name_capacity) ;
265- let mut ignored_mask = 0 ;
273+ if names. is_empty ( ) || self . matchers . is_empty ( ) {
274+ return ;
275+ }
276+ if self . is_directory_ignored ( relative_parent) {
277+ ignored. fill ( 1 ) ;
278+ return ;
279+ }
266280
267- for ( index, name) in names. iter ( ) . enumerate ( ) {
281+ let name_capacity = names. iter ( ) . map ( String :: len) . max ( ) . unwrap_or ( 0 ) ;
282+ let mut relative_path = String :: with_capacity ( relative_parent. len ( ) + 1 + name_capacity) ;
283+ // Resolve applicable sources and each source's parent state once for the whole batch.
284+ let root_ends = std:: iter:: once ( 0 )
285+ . chain ( relative_parent. match_indices ( '/' ) . map ( |( index, _) | index) )
286+ . chain ( ( !relative_parent. is_empty ( ) ) . then_some ( relative_parent. len ( ) ) ) ;
287+ for root_end in root_ends {
288+ let Some ( source) = self . matchers . get_mut ( & relative_parent[ ..root_end] ) else {
289+ continue ;
290+ } ;
291+ let source_start = if root_end == 0 {
292+ 0
293+ } else {
294+ ( root_end + 1 ) . min ( relative_parent. len ( ) )
295+ } ;
296+ let source_parent = & relative_parent[ source_start..] ;
297+ let parent_ignored =
298+ !source_parent. is_empty ( ) && source. is_directory_ignored ( source_parent) ;
268299 relative_path. clear ( ) ;
269- if !relative_parent . is_empty ( ) {
270- relative_path . push_str ( relative_parent ) ;
300+ relative_path . push_str ( source_parent ) ;
301+ if !source_parent . is_empty ( ) {
271302 relative_path. push ( '/' ) ;
272303 }
273- relative_path. push_str ( name) ;
274- if self . is_ignored ( & relative_path, directory_mask & ( 1 << index) != 0 ) {
275- ignored_mask |= 1 << index;
304+ let prefix_len = relative_path. len ( ) ;
305+ for ( index, name) in names. iter ( ) . enumerate ( ) {
306+ relative_path. truncate ( prefix_len) ;
307+ relative_path. push_str ( name) ;
308+ if let Some ( state) =
309+ source. match_path ( & relative_path, is_directory ( index) , Some ( parent_ignored) )
310+ {
311+ ignored[ index] = u8:: from ( state) ;
312+ }
276313 }
277314 }
278315
279- ignored_mask
316+ relative_path. clear ( ) ;
317+ relative_path. push_str ( relative_parent) ;
318+ if !relative_parent. is_empty ( ) {
319+ relative_path. push ( '/' ) ;
320+ }
321+ let prefix_len = relative_path. len ( ) ;
322+ for ( index, name) in names. iter ( ) . enumerate ( ) {
323+ if is_directory ( index) {
324+ relative_path. truncate ( prefix_len) ;
325+ relative_path. push_str ( name) ;
326+ self . ignored_directories
327+ . insert ( relative_path. clone ( ) . into_boxed_str ( ) , ignored[ index] != 0 ) ;
328+ }
329+ }
280330 }
281331
282332 fn invalidate_directory_cache ( & mut self , relative_root : & str ) {
@@ -314,7 +364,7 @@ impl GitIgnoreMatcher {
314364 if self . matchers . len ( ) == 1 {
315365 if let Some ( root_matcher) = self . matchers . get_mut ( "" ) {
316366 return root_matcher
317- . match_path ( relative_path, is_directory)
367+ . match_path ( relative_path, is_directory, None )
318368 . unwrap_or ( false ) ;
319369 }
320370 }
@@ -327,7 +377,7 @@ impl GitIgnoreMatcher {
327377 let matcher_root = & relative_path[ ..matcher_root_end] ;
328378 if let Some ( matcher) = self . matchers . get_mut ( matcher_root) {
329379 let path_from_matcher = & relative_path[ path_from_matcher_start..] ;
330- if let Some ( state) = matcher. match_path ( path_from_matcher, is_directory) {
380+ if let Some ( state) = matcher. match_path ( path_from_matcher, is_directory, None ) {
331381 ignored = state;
332382 }
333383 }
@@ -355,39 +405,36 @@ impl GitIgnoreSourceMatcher {
355405 }
356406 }
357407
358- fn match_path ( & mut self , relative_path : & str , is_directory : bool ) -> Option < bool > {
408+ fn match_path (
409+ & mut self ,
410+ relative_path : & str ,
411+ is_directory : bool ,
412+ // Batch callers can reuse the shared parent's state.
413+ parent_ignored : Option < bool > ,
414+ ) -> Option < bool > {
359415 if is_directory {
360- return self . match_directory ( relative_path) ;
361- }
362-
363- match self . matcher . matched ( relative_path, false ) {
364- Match :: Ignore ( _) => Some ( true ) ,
365- Match :: Whitelist ( _) => Some ( false ) ,
366- Match :: None => parent_directory ( relative_path)
367- . is_some_and ( |parent| self . is_directory_ignored ( parent) )
368- . then_some ( true ) ,
369- }
370- }
371-
372- fn match_directory ( & mut self , relative_path : & str ) -> Option < bool > {
373- if let Some ( state) = self . directory_states . get ( relative_path) {
374- return * state;
416+ if let Some ( state) = self . directory_states . get ( relative_path) {
417+ return * state;
418+ }
375419 }
376-
377- let matched = self . matcher . matched ( relative_path, true ) ;
378- let state = match matched {
420+ let state = match self . matcher . matched ( relative_path, is_directory) {
379421 Match :: Ignore ( _) => Some ( true ) ,
380422 Match :: Whitelist ( _) => Some ( false ) ,
381- Match :: None => parent_directory ( relative_path)
382- . is_some_and ( |parent| self . is_directory_ignored ( parent) )
423+ Match :: None => parent_ignored
424+ . unwrap_or_else ( || {
425+ parent_directory ( relative_path)
426+ . is_some_and ( |parent| self . is_directory_ignored ( parent) )
427+ } )
383428 . then_some ( true ) ,
384429 } ;
385- self . directory_states . insert ( relative_path. into ( ) , state) ;
430+ if is_directory {
431+ self . directory_states . insert ( relative_path. into ( ) , state) ;
432+ }
386433 state
387434 }
388435
389436 fn is_directory_ignored ( & mut self , relative_path : & str ) -> bool {
390- self . match_directory ( relative_path) == Some ( true )
437+ self . match_path ( relative_path, true , None ) == Some ( true )
391438 }
392439}
393440
@@ -421,6 +468,7 @@ impl SourceMatcher {
421468 ignored : & mut [ u8 ] ,
422469 ) -> usize {
423470 let relative_parent = self . relative_path ( parent_path) ;
471+ let parent_ignored = self . is_directory_ignored ( & to_posix_path ( relative_parent. as_ref ( ) ) ) ;
424472 let name_capacity = names. iter ( ) . map ( String :: len) . max ( ) . unwrap_or ( 0 ) ;
425473 let mut relative_path = PathBuf :: with_capacity (
426474 relative_parent. as_os_str ( ) . len ( )
@@ -437,7 +485,8 @@ impl SourceMatcher {
437485 relative_path. clear ( ) ;
438486 relative_path. push ( relative_parent. as_ref ( ) ) ;
439487 relative_path. push ( name) ;
440- if self . is_relative_path_ignored ( & relative_path, directory_flags[ index] != 0 ) {
488+ if parent_ignored || self . is_child_ignored ( & relative_path, directory_flags[ index] != 0 )
489+ {
441490 ignored[ index] = 1 ;
442491 matched += 1 ;
443492 }
@@ -456,6 +505,7 @@ impl SourceMatcher {
456505 debug_assert_ne ! ( candidate_mask, 0 ) ;
457506
458507 let relative_parent = self . relative_path ( parent_path) ;
508+ let parent_ignored = self . is_directory_ignored ( & to_posix_path ( relative_parent. as_ref ( ) ) ) ;
459509 let name_capacity = names[ candidate_mask. trailing_zeros ( ) as usize ] . len ( ) ;
460510 let mut relative_path = PathBuf :: with_capacity (
461511 relative_parent. as_os_str ( ) . len ( )
@@ -473,14 +523,26 @@ impl SourceMatcher {
473523 relative_path. clear ( ) ;
474524 relative_path. push ( relative_parent. as_ref ( ) ) ;
475525 relative_path. push ( & names[ index] ) ;
476- if self . is_relative_path_ignored ( & relative_path, directory_mask & entry_mask != 0 ) {
526+ if parent_ignored
527+ || self . is_child_ignored ( & relative_path, directory_mask & entry_mask != 0 )
528+ {
477529 ignored_mask |= entry_mask;
478530 }
479531 }
480532
481533 ignored_mask
482534 }
483535
536+ // Batch callers have already checked whether the shared parent is ignored.
537+ fn is_child_ignored ( & mut self , relative_path : & Path , is_directory : bool ) -> bool {
538+ let relative_path = to_posix_path ( relative_path) ;
539+ if is_directory {
540+ self . is_directory_ignored ( & relative_path)
541+ } else {
542+ is_ignore_match ( self . matcher . matched ( relative_path. as_ref ( ) , false ) )
543+ }
544+ }
545+
484546 fn is_relative_path_ignored ( & mut self , relative_path : & Path , is_directory : bool ) -> bool {
485547 if relative_path. as_os_str ( ) . is_empty ( ) {
486548 return false ;
@@ -691,6 +753,22 @@ mod tests {
691753
692754 assert ! ( !matcher. is_ignored( "scripts/debug" , true ) ) ;
693755 assert ! ( matcher. is_ignored( "scripts/debug/launch.mjs" , false ) ) ;
756+
757+ let names = vec ! [ "keep.js" . into( ) , "drop.js" . into( ) , "nested" . into( ) ] ;
758+ assert_eq ! (
759+ matcher. is_ignored_batch( "scripts/debug" , & names, & [ 0 , 0 , 1 ] ) ,
760+ vec![ 1 , 1 , 1 ]
761+ ) ;
762+ assert ! ( matcher. is_ignored( "scripts/debug/nested" , true ) ) ;
763+
764+ matcher
765+ . add_source ( "scripts/debug" , "!keep.js\n !nested/\n " )
766+ . unwrap ( ) ;
767+ assert_eq ! (
768+ matcher. is_ignored_batch_mask( "scripts/debug" , & names, 0b100 ) ,
769+ 0b010
770+ ) ;
771+ assert ! ( !matcher. is_ignored( "scripts/debug/nested" , true ) ) ;
694772 }
695773
696774 #[ test]
0 commit comments