@@ -90,13 +90,22 @@ pub fn process_entries(args: &Cli) -> Result<Vec<FileEntry>> {
9090 . git_ignore ( !args. no_ignore )
9191 . ignore ( !args. no_ignore ) ;
9292
93+ let mut override_builder = OverrideBuilder :: new ( path) ;
94+
95+ // Handle include patterns first (positive patterns)
9396 if let Some ( ref includes) = args. include {
9497 for pattern in includes {
95- builder. add_custom_ignore_filename ( pattern) ;
98+ // Include patterns are positive patterns (no ! prefix)
99+ if let Err ( e) = override_builder. add ( pattern) {
100+ eprintln ! (
101+ "Warning: Invalid include pattern '{}': {}" ,
102+ pattern, e
103+ ) ;
104+ }
96105 }
97106 }
98107
99- let mut override_builder = OverrideBuilder :: new ( path ) ;
108+ // Handle exclude patterns (negative patterns)
100109 if let Some ( ref excludes) = args. exclude {
101110 for exclude in excludes {
102111 match exclude {
@@ -177,15 +186,37 @@ pub fn process_entries(args: &Cli) -> Result<Vec<FileEntry>> {
177186 . map ( |m| m. len ( ) <= max_size)
178187 . unwrap_or ( false )
179188 {
180- // Need to check excludes even for single files explicitly passed
189+ // Need to check includes and excludes even for single files explicitly passed
181190 let mut excluded = false ;
191+ let mut override_builder =
192+ OverrideBuilder :: new ( path. parent ( ) . unwrap_or ( path) ) ; // Base relative to parent
193+
194+ // Handle include patterns first (positive patterns)
195+ if let Some ( ref includes) = args. include {
196+ for pattern in includes {
197+ // Include patterns are positive patterns (no ! prefix)
198+ if let Err ( e) = override_builder. add ( pattern) {
199+ eprintln ! (
200+ "Warning: Invalid include pattern '{}': {}" ,
201+ pattern, e
202+ ) ;
203+ }
204+ }
205+ }
206+
207+ // Handle exclude patterns (negative patterns)
182208 if let Some ( ref excludes) = args. exclude {
183- let mut override_builder =
184- OverrideBuilder :: new ( path. parent ( ) . unwrap_or ( path) ) ; // Base relative to parent
185209 for exclude in excludes {
186210 match exclude {
187211 Exclude :: Pattern ( pattern) => {
188- if let Err ( e) = override_builder. add ( pattern) {
212+ // Add a '!' prefix if it doesn't already have one
213+ // This makes it a negative pattern (exclude)
214+ let exclude_pattern = if !pattern. starts_with ( '!' ) {
215+ format ! ( "!{}" , pattern)
216+ } else {
217+ pattern. clone ( )
218+ } ;
219+ if let Err ( e) = override_builder. add ( & exclude_pattern) {
189220 eprintln ! (
190221 "Warning: Invalid exclude pattern '{}': {}" ,
191222 pattern, e
@@ -203,10 +234,19 @@ pub fn process_entries(args: &Cli) -> Result<Vec<FileEntry>> {
203234 if excluded {
204235 continue ;
205236 }
206- let overrides = override_builder. build ( ) ?;
207- if overrides. matched ( path, false ) . is_ignore ( ) {
208- excluded = true ;
209- }
237+ }
238+
239+ let overrides = override_builder. build ( ) ?;
240+ let match_result = overrides. matched ( path, false ) ;
241+
242+ // If there are include patterns, the file must match at least one include pattern
243+ // and not be excluded by any exclude pattern
244+ if args. include . is_some ( ) {
245+ // With include patterns: file must be whitelisted (matched by include) and not ignored (excluded)
246+ excluded = !match_result. is_whitelist ( ) || match_result. is_ignore ( ) ;
247+ } else {
248+ // Without include patterns: file is excluded only if it matches an exclude pattern
249+ excluded = match_result. is_ignore ( ) ;
210250 }
211251
212252 if !excluded {
@@ -398,18 +438,36 @@ mod tests {
398438
399439 // Test excluding all Rust files
400440 cli. exclude = Some ( vec ! [ Exclude :: Pattern ( "**/*.rs" . to_string( ) ) ] ) ;
401- let _ = process_directory ( & cli) ?;
441+ let entries = process_entries ( & cli) ?;
442+
402443 // Verify no .rs files were processed
403- // This would need to be adapted based on how you want to verify the results
444+ for entry in & entries {
445+ assert ! (
446+ entry. path. extension( ) . and_then( |ext| ext. to_str( ) ) != Some ( "rs" ) ,
447+ "Found .rs file that should have been excluded: {:?}" ,
448+ entry. path
449+ ) ;
450+ }
404451
405452 // Test excluding specific directories
406453 cli. exclude = Some ( vec ! [
407454 Exclude :: Pattern ( "**/node_modules/**" . to_string( ) ) ,
408455 Exclude :: Pattern ( "**/target/**" . to_string( ) ) ,
409456 Exclude :: Pattern ( "**/.git/**" . to_string( ) ) ,
410457 ] ) ;
411- let _ = process_directory ( & cli) ?;
458+ let entries = process_entries ( & cli) ?;
459+
412460 // Verify excluded directories were not processed
461+ for entry in & entries {
462+ let path_str = entry. path . to_string_lossy ( ) ;
463+ assert ! (
464+ !path_str. contains( "node_modules" ) &&
465+ !path_str. contains( "target" ) &&
466+ !path_str. contains( ".git" ) ,
467+ "Found file from excluded directory: {:?}" ,
468+ entry. path
469+ ) ;
470+ }
413471
414472 Ok ( ( ) )
415473 }
@@ -421,13 +479,93 @@ mod tests {
421479
422480 // Test including only Rust files
423481 cli. include = Some ( vec ! [ "**/*.rs" . to_string( ) ] ) ;
424- let _ = process_directory ( & cli) ?;
482+ let entries = process_entries ( & cli) ?;
483+
425484 // Verify only .rs files were processed
485+ assert ! ( !entries. is_empty( ) , "Should have found some .rs files" ) ;
486+ for entry in & entries {
487+ assert ! (
488+ entry. path. extension( ) . and_then( |ext| ext. to_str( ) ) == Some ( "rs" ) ,
489+ "Found non-.rs file: {:?}" ,
490+ entry. path
491+ ) ;
492+ }
493+
494+ // Should find 4 .rs files: main.rs, lib.rs, test.rs, code.rs
495+ assert_eq ! ( entries. len( ) , 4 , "Should find exactly 4 .rs files" ) ;
426496
427497 // Test including multiple patterns
428498 cli. include = Some ( vec ! [ "**/*.rs" . to_string( ) , "**/*.py" . to_string( ) ] ) ;
429- let _ = process_directory ( & cli) ?;
499+ let entries = process_entries ( & cli) ?;
500+
430501 // Verify only .rs and .py files were processed
502+ assert ! ( !entries. is_empty( ) , "Should have found some .rs and .py files" ) ;
503+ for entry in & entries {
504+ let ext = entry. path . extension ( ) . and_then ( |ext| ext. to_str ( ) ) ;
505+ assert ! (
506+ ext == Some ( "rs" ) || ext == Some ( "py" ) ,
507+ "Found file with unexpected extension: {:?}" ,
508+ entry. path
509+ ) ;
510+ }
511+
512+ // Should find 4 .rs files + 1 .py file = 5 total
513+ assert_eq ! ( entries. len( ) , 5 , "Should find exactly 5 .rs and .py files" ) ;
514+
515+ Ok ( ( ) )
516+ }
517+
518+ #[ test]
519+ fn test_process_directory_with_includes_and_excludes ( ) -> Result < ( ) > {
520+ let ( dir, _files) = setup_test_directory ( ) ?;
521+ let mut cli = create_test_cli ( dir. path ( ) ) ;
522+
523+ // Test including only Rust files but excluding specific ones
524+ cli. include = Some ( vec ! [ "**/*.rs" . to_string( ) ] ) ;
525+ cli. exclude = Some ( vec ! [ Exclude :: Pattern ( "**/test.rs" . to_string( ) ) ] ) ;
526+ let entries = process_entries ( & cli) ?;
527+
528+ // Verify only .rs files were processed, but test.rs was excluded
529+ assert ! ( !entries. is_empty( ) , "Should have found some .rs files" ) ;
530+ for entry in & entries {
531+ assert ! (
532+ entry. path. extension( ) . and_then( |ext| ext. to_str( ) ) == Some ( "rs" ) ,
533+ "Found non-.rs file: {:?}" ,
534+ entry. path
535+ ) ;
536+ assert ! (
537+ !entry. path. to_string_lossy( ) . contains( "test.rs" ) ,
538+ "Found excluded test.rs file: {:?}" ,
539+ entry. path
540+ ) ;
541+ }
542+
543+ // Should find 3 .rs files (main.rs, lib.rs, code.rs) but not test.rs
544+ assert_eq ! ( entries. len( ) , 3 , "Should find exactly 3 .rs files (excluding test.rs)" ) ;
545+
546+ // Test including multiple file types but excluding a directory
547+ cli. include = Some ( vec ! [ "**/*.rs" . to_string( ) , "**/*.py" . to_string( ) ] ) ;
548+ cli. exclude = Some ( vec ! [ Exclude :: Pattern ( "**/nested/**" . to_string( ) ) ] ) ;
549+ let entries = process_entries ( & cli) ?;
550+
551+ // Verify only .rs and .py files were processed, but nested directory was excluded
552+ assert ! ( !entries. is_empty( ) , "Should have found some .rs and .py files" ) ;
553+ for entry in & entries {
554+ let ext = entry. path . extension ( ) . and_then ( |ext| ext. to_str ( ) ) ;
555+ assert ! (
556+ ext == Some ( "rs" ) || ext == Some ( "py" ) ,
557+ "Found file with unexpected extension: {:?}" ,
558+ entry. path
559+ ) ;
560+ assert ! (
561+ !entry. path. to_string_lossy( ) . contains( "nested" ) ,
562+ "Found file from excluded nested directory: {:?}" ,
563+ entry. path
564+ ) ;
565+ }
566+
567+ // Should find 3 .rs files (main.rs, lib.rs, test.rs) but not code.rs or script.py from nested
568+ assert_eq ! ( entries. len( ) , 3 , "Should find exactly 3 files (excluding nested directory)" ) ;
431569
432570 Ok ( ( ) )
433571 }
0 commit comments