11use std:: borrow:: Cow ;
2+ use std:: collections:: HashSet ;
23use std:: ffi:: OsStr ;
34use std:: io:: { self , Write } ;
45use std:: mem;
@@ -344,20 +345,22 @@ impl WorkerState {
344345 . map_err ( |_| anyhow ! ( "Mismatch in exclude patterns" ) )
345346 }
346347
347- fn build_walker ( & self , paths : & [ PathBuf ] ) -> Result < WalkParallel > {
348+ fn build_walker ( & self , paths : & [ PathBuf ] , respect_vcs_ignore : bool ) -> Result < WalkParallel > {
348349 let first_path = & paths[ 0 ] ;
349350 let config = & self . config ;
350351 let overrides = self . build_overrides ( paths) ?;
351352
353+ let use_vcs_ignore = respect_vcs_ignore && config. read_vcsignore ;
354+
352355 let mut builder = WalkBuilder :: new ( first_path) ;
353356 builder
354357 . hidden ( config. ignore_hidden )
355358 . ignore ( config. read_fdignore )
356- . parents ( config. read_parent_ignore && ( config. read_fdignore || config . read_vcsignore ) )
357- . git_ignore ( config . read_vcsignore )
358- . git_global ( config . read_vcsignore )
359- . git_exclude ( config . read_vcsignore )
360- . require_git ( config. require_git_to_read_vcsignore )
359+ . parents ( config. read_parent_ignore && ( config. read_fdignore || use_vcs_ignore ) )
360+ . git_ignore ( use_vcs_ignore )
361+ . git_global ( use_vcs_ignore )
362+ . git_exclude ( use_vcs_ignore )
363+ . require_git ( respect_vcs_ignore && config. require_git_to_read_vcsignore )
361364 . overrides ( overrides)
362365 . follow_links ( config. follow_links )
363366 // No need to check for supported platforms, option is unavailable on unsupported ones
@@ -403,6 +406,231 @@ impl WorkerState {
403406 Ok ( walker)
404407 }
405408
409+ /// Build an Override matcher for checking if entries match --override-ignore patterns.
410+ fn build_override_matcher ( & self , paths : & [ PathBuf ] ) -> Result < Override > {
411+ let first_path = & paths[ 0 ] ;
412+ let mut builder = OverrideBuilder :: new ( first_path) ;
413+ for pattern in & self . config . override_ignore_patterns {
414+ builder
415+ . add ( pattern)
416+ . map_err ( |e| anyhow ! ( "Malformed override-ignore pattern: {}" , e) ) ?;
417+ // Also match contents inside directories matching the pattern
418+ let dir_contents = format ! ( "{pattern}/**" ) ;
419+ builder
420+ . add ( & dir_contents)
421+ . map_err ( |e| anyhow ! ( "Malformed override-ignore pattern: {}" , e) ) ?;
422+ }
423+ builder
424+ . build ( )
425+ . map_err ( |_| anyhow ! ( "Mismatch in override-ignore patterns" ) )
426+ }
427+
428+ /// Spawn sender threads for the override walk (walk 2).
429+ /// Only emits entries that match override-ignore patterns and weren't already seen.
430+ fn spawn_override_senders (
431+ & self ,
432+ walker : WalkParallel ,
433+ tx : Sender < Batch > ,
434+ seen_paths : & Mutex < HashSet < PathBuf > > ,
435+ override_matcher : & Override ,
436+ ) {
437+ walker. run ( || {
438+ let patterns = & self . patterns ;
439+ let config = & self . config ;
440+ let quit_flag = self . quit_flag . as_ref ( ) ;
441+
442+ let mut limit = 0x100 ;
443+ if let Some ( cmd) = & config. command
444+ && !cmd. in_batch_mode ( )
445+ && config. threads > 1
446+ {
447+ limit = 1 ;
448+ }
449+ let mut tx = BatchSender :: new ( tx. clone ( ) , limit) ;
450+
451+ Box :: new ( move |entry| {
452+ if quit_flag. load ( Ordering :: Relaxed ) {
453+ return WalkState :: Quit ;
454+ }
455+
456+ if let Ok ( e) = & entry {
457+ let entry_path = e. path ( ) ;
458+ if entry_path. is_dir ( )
459+ && config
460+ . ignore_contain
461+ . iter ( )
462+ . any ( |ic| entry_path. join ( ic) . exists ( ) )
463+ {
464+ return WalkState :: Skip ;
465+ }
466+ if e. depth ( ) == 0 {
467+ return WalkState :: Continue ;
468+ }
469+ }
470+
471+ let entry = match entry {
472+ Ok ( e) => DirEntry :: normal ( e) ,
473+ Err ( ignore:: Error :: WithPath {
474+ path,
475+ err : inner_err,
476+ } ) => match inner_err. as_ref ( ) {
477+ ignore:: Error :: Io ( io_error)
478+ if io_error. kind ( ) == io:: ErrorKind :: NotFound
479+ && path
480+ . symlink_metadata ( )
481+ . ok ( )
482+ . is_some_and ( |m| m. file_type ( ) . is_symlink ( ) ) =>
483+ {
484+ DirEntry :: broken_symlink ( path)
485+ }
486+ _ => {
487+ return match tx. send ( WorkerResult :: Error ( ignore:: Error :: WithPath {
488+ path,
489+ err : inner_err,
490+ } ) ) {
491+ Ok ( _) => WalkState :: Continue ,
492+ Err ( _) => WalkState :: Quit ,
493+ } ;
494+ }
495+ } ,
496+ Err ( err) => {
497+ return match tx. send ( WorkerResult :: Error ( err) ) {
498+ Ok ( _) => WalkState :: Continue ,
499+ Err ( _) => WalkState :: Quit ,
500+ } ;
501+ }
502+ } ;
503+
504+ // Check override-ignore pattern match.
505+ // Return Continue (not Skip) so directories are still traversed
506+ // even when they don't match — files inside them might match.
507+ let entry_path = entry. path ( ) ;
508+ let is_dir = entry_path. is_dir ( ) ;
509+ if !override_matcher. matched ( entry_path, is_dir) . is_whitelist ( ) {
510+ return WalkState :: Continue ;
511+ }
512+
513+ // Dedup: skip entries already found in walk 1
514+ if seen_paths
515+ . lock ( )
516+ . unwrap ( )
517+ . contains ( & entry_path. to_path_buf ( ) )
518+ {
519+ return WalkState :: Continue ;
520+ }
521+
522+ if let Some ( min_depth) = config. min_depth
523+ && entry. depth ( ) . is_none_or ( |d| d < min_depth)
524+ {
525+ return WalkState :: Continue ;
526+ }
527+
528+ let search_str: Cow < OsStr > = if config. search_full_path {
529+ let path_abs_buf = filesystem:: path_absolute_form ( entry_path)
530+ . expect ( "Retrieving absolute path succeeds" ) ;
531+ Cow :: Owned ( path_abs_buf. as_os_str ( ) . to_os_string ( ) )
532+ } else {
533+ match entry_path. file_name ( ) {
534+ Some ( filename) => Cow :: Borrowed ( filename) ,
535+ None => unreachable ! (
536+ "Encountered file system entry without a file name. This should only \
537+ happen for paths like 'foo/bar/..' or '/' which are not supposed to \
538+ appear in a file system traversal."
539+ ) ,
540+ }
541+ } ;
542+
543+ if !patterns
544+ . iter ( )
545+ . all ( |pat| pat. is_match ( & filesystem:: osstr_to_bytes ( search_str. as_ref ( ) ) ) )
546+ {
547+ return WalkState :: Continue ;
548+ }
549+
550+ if let Some ( ref exts_regex) = config. extensions {
551+ if let Some ( path_str) = entry_path. file_name ( ) {
552+ if !exts_regex. is_match ( & filesystem:: osstr_to_bytes ( path_str) ) {
553+ return WalkState :: Continue ;
554+ }
555+ } else {
556+ return WalkState :: Continue ;
557+ }
558+ }
559+
560+ if let Some ( ref file_types) = config. file_types
561+ && file_types. should_ignore ( & entry)
562+ {
563+ return WalkState :: Continue ;
564+ }
565+
566+ #[ cfg( unix) ]
567+ {
568+ if let Some ( ref owner_constraint) = config. owner_constraint {
569+ if let Some ( metadata) = entry. metadata ( ) {
570+ if !owner_constraint. matches ( metadata) {
571+ return WalkState :: Continue ;
572+ }
573+ } else {
574+ return WalkState :: Continue ;
575+ }
576+ }
577+ }
578+
579+ if !config. size_constraints . is_empty ( ) {
580+ if entry_path. is_file ( ) {
581+ if let Some ( metadata) = entry. metadata ( ) {
582+ let file_size = metadata. len ( ) ;
583+ if config
584+ . size_constraints
585+ . iter ( )
586+ . any ( |sc| !sc. is_within ( file_size) )
587+ {
588+ return WalkState :: Continue ;
589+ }
590+ } else {
591+ return WalkState :: Continue ;
592+ }
593+ } else {
594+ return WalkState :: Continue ;
595+ }
596+ }
597+
598+ if !config. time_constraints . is_empty ( ) {
599+ let mut matched = false ;
600+ if let Some ( metadata) = entry. metadata ( )
601+ && let Ok ( modified) = metadata. modified ( )
602+ {
603+ matched = config
604+ . time_constraints
605+ . iter ( )
606+ . all ( |tf| tf. applies_to ( & modified) ) ;
607+ }
608+ if !matched {
609+ return WalkState :: Continue ;
610+ }
611+ }
612+
613+ if config. is_printing ( )
614+ && let Some ( ls_colors) = & config. ls_colors
615+ {
616+ entry. style ( ls_colors) ;
617+ }
618+
619+ let send_result = tx. send ( WorkerResult :: Entry ( entry) ) ;
620+
621+ if send_result. is_err ( ) {
622+ return WalkState :: Quit ;
623+ }
624+
625+ if config. prune {
626+ return WalkState :: Skip ;
627+ }
628+
629+ WalkState :: Continue
630+ } )
631+ } ) ;
632+ }
633+
406634 /// Run the receiver work, either on this thread or a pool of background
407635 /// threads (for --exec).
408636 fn receive ( & self , rx : Receiver < Batch > ) -> ExitCode {
@@ -440,7 +668,12 @@ impl WorkerState {
440668 }
441669
442670 /// Spawn the sender threads.
443- fn spawn_senders ( & self , walker : WalkParallel , tx : Sender < Batch > ) {
671+ fn spawn_senders (
672+ & self ,
673+ walker : WalkParallel ,
674+ tx : Sender < Batch > ,
675+ seen_paths : Option < & Mutex < HashSet < PathBuf > > > ,
676+ ) {
444677 walker. run ( || {
445678 let patterns = & self . patterns ;
446679 let config = & self . config ;
@@ -614,6 +847,11 @@ impl WorkerState {
614847 entry. style ( ls_colors) ;
615848 }
616849
850+ // Track seen paths for dedup with override walk
851+ if let Some ( seen) = seen_paths {
852+ seen. lock ( ) . unwrap ( ) . insert ( entry. path ( ) . to_path_buf ( ) ) ;
853+ }
854+
617855 let send_result = tx. send ( WorkerResult :: Entry ( entry) ) ;
618856
619857 if send_result. is_err ( ) {
@@ -633,7 +871,7 @@ impl WorkerState {
633871 /// Perform the recursive scan.
634872 fn scan ( & self , paths : & [ PathBuf ] ) -> Result < ExitCode > {
635873 let config = & self . config ;
636- let walker = self . build_walker ( paths) ?;
874+ let walker = self . build_walker ( paths, true ) ?;
637875
638876 if config. ls_colors . is_some ( ) && config. is_printing ( ) {
639877 let quit_flag = Arc :: clone ( & self . quit_flag ) ;
@@ -650,14 +888,41 @@ impl WorkerState {
650888 . unwrap ( ) ;
651889 }
652890
891+ let has_overrides = !config. override_ignore_patterns . is_empty ( ) ;
892+ let seen_paths: Option < Mutex < HashSet < PathBuf > > > = if has_overrides {
893+ Some ( Mutex :: new ( HashSet :: new ( ) ) )
894+ } else {
895+ None
896+ } ;
897+
653898 let ( tx, rx) = bounded ( 2 * config. threads ) ;
654899
655900 let exit_code = thread:: scope ( |scope| {
656901 // Spawn the receiver thread(s)
657902 let receiver = scope. spawn ( || self . receive ( rx) ) ;
658903
659- // Spawn the sender threads.
660- self . spawn_senders ( walker, tx) ;
904+ // Walk 1: normal walk (respects all ignore rules)
905+ let tx1 = tx. clone ( ) ;
906+ self . spawn_senders ( walker, tx1, seen_paths. as_ref ( ) ) ;
907+
908+ // Walk 2: override walk (disables VCS ignores, only emits
909+ // entries matching --override-ignore patterns not seen in walk 1)
910+ if has_overrides
911+ && !self . quit_flag . load ( Ordering :: Relaxed )
912+ && let Ok ( override_walker) = self . build_walker ( paths, false )
913+ && let Ok ( override_matcher) = self . build_override_matcher ( paths)
914+ {
915+ let tx2 = tx. clone ( ) ;
916+ self . spawn_override_senders (
917+ override_walker,
918+ tx2,
919+ seen_paths. as_ref ( ) . unwrap ( ) ,
920+ & override_matcher,
921+ ) ;
922+ }
923+
924+ // Drop our copy of tx to signal receiver we're done
925+ drop ( tx) ;
661926
662927 receiver. join ( ) . unwrap ( )
663928 } ) ;
0 commit comments