11use crate :: { LogError , SourceLanguage } ;
2+ use ignore:: gitignore:: { Gitignore , GitignoreBuilder } ;
23use serde:: { Deserialize , Serialize } ;
34use std:: cell:: RefCell ;
45use std:: collections:: BTreeMap ;
@@ -11,6 +12,16 @@ fn is_ignored_dir(name: &OsStr) -> bool {
1112 name == ".git" || name == ".hg" || name == ".svn" || name == ".vscode"
1213}
1314
15+ fn build_gitignore ( root : & Path ) -> Gitignore {
16+ let mut builder = GitignoreBuilder :: new ( root) ;
17+ builder. add ( root. join ( ".gitignore" ) ) ;
18+ builder. build ( ) . unwrap_or_else ( |_| Gitignore :: empty ( ) )
19+ }
20+
21+ fn is_gitignored ( gi : & Gitignore , path : & Path , is_dir : bool ) -> bool {
22+ gi. matched_path_or_any_parents ( path, is_dir) . is_ignore ( )
23+ }
24+
1425/// Result of a shallow check of a file system path. Mainly interested in getting a directory
1526/// listing without descending into the child trees.
1627enum ShallowCheckResult {
@@ -80,16 +91,26 @@ impl SourceHierContent {
8091 . collect ( ) )
8192 }
8293
83- fn from_dir ( path : & Path ) -> Self {
94+ fn from_dir ( path : & Path , gi : & Gitignore ) -> Self {
8495 match Self :: entries_of ( path) {
8596 Ok ( entries) => Self :: Directory {
8697 entries : entries
8798 . into_iter ( )
88- . filter ( |entry| !is_ignored_dir ( & entry. 0 ) )
99+ . filter ( |entry| {
100+ !is_ignored_dir ( & entry. 0 ) && {
101+ let child_path = path. join ( & entry. 0 ) ;
102+ let is_dir = entry
103+ . 1
104+ . as_ref ( )
105+ . map ( |m| m. is_dir ( ) )
106+ . unwrap_or ( false ) ;
107+ !is_gitignored ( gi, & child_path, is_dir)
108+ }
109+ } )
89110 . map ( |( entry_name, meta) | {
90111 (
91112 entry_name. to_os_string ( ) ,
92- SourceHierNode :: from_int ( & path. join ( entry_name) , meta) ,
113+ SourceHierNode :: from_int ( & path. join ( entry_name) , meta, gi ) ,
93114 )
94115 } )
95116 . collect ( ) ,
@@ -103,11 +124,11 @@ impl SourceHierContent {
103124 }
104125 }
105126
106- fn from ( path : & Path , metadata : Result < fs:: Metadata , io:: Error > ) -> Self {
127+ fn from ( path : & Path , metadata : Result < fs:: Metadata , io:: Error > , gi : & Gitignore ) -> Self {
107128 match metadata {
108129 Ok ( meta) => {
109130 if meta. is_dir ( ) {
110- Self :: from_dir ( path)
131+ Self :: from_dir ( path, gi )
111132 } else if meta. is_file ( ) {
112133 match SourceLanguage :: from_path ( & path) {
113134 Some ( language) => match meta. modified ( ) {
@@ -183,6 +204,7 @@ impl SourceHierContent {
183204 path : & Path ,
184205 latest_meta : Result < fs:: Metadata , io:: Error > ,
185206 deleted_events : & mut Vec < ScanEvent > ,
207+ gi : & Gitignore ,
186208 ) -> bool {
187209 let latest_content = Self :: shallow_check ( path, & latest_meta) ;
188210 * self = match self {
@@ -199,7 +221,7 @@ impl SourceHierContent {
199221 }
200222 _ => {
201223 deleted_events. push ( ScanEvent :: DeletedFile ( PathBuf :: from ( path) , info. id ) ) ;
202- Self :: from ( path, latest_meta)
224+ Self :: from ( path, latest_meta, gi )
203225 }
204226 } ,
205227 SourceHierContent :: Directory { ref mut entries } => match latest_content {
@@ -216,23 +238,27 @@ impl SourceHierContent {
216238 let mut new_entries: Vec < ( OsString , Result < fs:: Metadata , io:: Error > ) > =
217239 Vec :: new ( ) ;
218240 for ( name, meta) in latest_entries {
219- if is_ignored_dir ( & name. as_os_str ( ) ) {
241+ let child_path = path. join ( & name) ;
242+ let is_dir = meta. as_ref ( ) . map ( |m| m. is_dir ( ) ) . unwrap_or ( false ) ;
243+ if is_ignored_dir ( & name. as_os_str ( ) )
244+ || is_gitignored ( gi, & child_path, is_dir)
245+ {
220246 } else if let Some ( existing_entry) = entries. get_mut ( & name) {
221- existing_entry. sync ( & path . join ( & name ) , meta, deleted_events)
247+ existing_entry. sync ( & child_path , meta, deleted_events, gi )
222248 } else {
223249 new_entries. push ( ( name, meta) ) ;
224250 changed = true ;
225251 }
226252 }
227253 new_entries. into_iter ( ) . for_each ( |( name, meta) | {
228- let node = SourceHierNode :: from_int ( & path. join ( & name) , meta) ;
254+ let node = SourceHierNode :: from_int ( & path. join ( & name) , meta, gi ) ;
229255 entries. insert ( name, node) ;
230256 } ) ;
231257 return changed;
232258 }
233- _ => Self :: from ( path, latest_meta) ,
259+ _ => Self :: from ( path, latest_meta, gi ) ,
234260 } ,
235- _ => Self :: from ( path, latest_meta) ,
261+ _ => Self :: from ( path, latest_meta, gi ) ,
236262 } ;
237263 true
238264 }
@@ -275,13 +301,13 @@ pub struct SourceHierNode {
275301}
276302
277303impl SourceHierNode {
278- fn from_int ( path : & Path , metadata : Result < fs:: Metadata , io:: Error > ) -> Self {
304+ fn from_int ( path : & Path , metadata : Result < fs:: Metadata , io:: Error > , gi : & Gitignore ) -> Self {
279305 match metadata {
280306 Ok ( meta) => {
281307 if meta. is_dir ( ) {
282308 Self {
283309 last_scan_time : None ,
284- content : SourceHierContent :: from_dir ( path) ,
310+ content : SourceHierContent :: from_dir ( path, gi ) ,
285311 }
286312 } else if meta. is_file ( ) {
287313 match SourceLanguage :: from_path ( & path) {
@@ -357,8 +383,9 @@ impl SourceHierNode {
357383 path : & Path ,
358384 meta : Result < fs:: Metadata , io:: Error > ,
359385 deleted_events : & mut Vec < ScanEvent > ,
386+ gi : & Gitignore ,
360387 ) {
361- if self . content . sync_int ( path, meta, deleted_events) {
388+ if self . content . sync_int ( path, meta, deleted_events, gi ) {
362389 self . last_scan_time = None ;
363390 }
364391 }
@@ -446,13 +473,15 @@ impl SourceHierTree {
446473
447474 /// Synchronize the state of this tree with the file system.
448475 pub fn sync ( & mut self ) {
476+ let gi = build_gitignore ( & self . root_path ) ;
449477 SourceFileInfo :: NEXT_ID . with ( |id_opt| {
450478 * id_opt. borrow_mut ( ) = self . next_id ;
451479 } ) ;
452480 self . root_node . sync (
453481 & self . root_path ,
454482 fs:: metadata ( & self . root_path ) ,
455483 & mut self . deleted_events ,
484+ & gi,
456485 ) ;
457486 self . next_id = SourceFileInfo :: NEXT_ID . with ( |id_opt| * id_opt. borrow ( ) ) ;
458487 self . stats = self . compute_stats ( ) ;
@@ -617,4 +646,51 @@ mod test {
617646 let deleted_dir_events: Vec < ScanEvent > = tree. scan ( ) . map ( redact_event) . collect ( ) ;
618647 assert_yaml_snapshot ! ( deleted_dir_events) ;
619648 }
649+
650+ #[ test]
651+ fn test_gitignore_filtering ( ) {
652+ let temp_dir = tempdir ( ) . expect ( "Failed to create temporary directory" ) ;
653+ let root = temp_dir. path ( ) ;
654+
655+ // Create a .gitignore that ignores *.log files and the "build/" directory
656+ let mut gitignore = File :: create ( root. join ( ".gitignore" ) ) . unwrap ( ) ;
657+ writeln ! ( gitignore, "*.log" ) . unwrap ( ) ;
658+ writeln ! ( gitignore, "build/" ) . unwrap ( ) ;
659+ drop ( gitignore) ;
660+
661+ // Create files: one that should be found, ones that should be ignored
662+ fs:: create_dir ( root. join ( "src" ) ) . unwrap ( ) ;
663+ File :: create ( root. join ( "src/main.rs" ) )
664+ . unwrap ( )
665+ . write ( b"fn main() {}" )
666+ . unwrap ( ) ;
667+ File :: create ( root. join ( "debug.log" ) )
668+ . unwrap ( )
669+ . write ( b"some log" )
670+ . unwrap ( ) ;
671+ fs:: create_dir ( root. join ( "build" ) ) . unwrap ( ) ;
672+ File :: create ( root. join ( "build/output.rs" ) )
673+ . unwrap ( )
674+ . write ( b"generated" )
675+ . unwrap ( ) ;
676+
677+ let mut tree = SourceHierTree :: from ( root) ;
678+ tree. sync ( ) ;
679+ let events: Vec < ScanEvent > = tree. scan ( ) . map ( redact_event) . collect ( ) ;
680+
681+ // Only main.rs should appear as a NewFile event
682+ assert ! ( events
683+ . iter( )
684+ . any( |e| matches!( e, ScanEvent :: NewFile ( p, _) if p == Path :: new( "main.rs" ) ) ) ) ;
685+ assert ! ( !events
686+ . iter( )
687+ . any( |e| matches!( e, ScanEvent :: NewFile ( p, _) if p == Path :: new( "debug.log" ) ) ) ) ;
688+ assert ! ( !events
689+ . iter( )
690+ . any( |e| matches!( e, ScanEvent :: NewFile ( p, _) if p == Path :: new( "output.rs" ) ) ) ) ;
691+
692+ // Verify stats don't count ignored files
693+ let stats = tree. stats ( ) ;
694+ assert_eq ! ( stats. files, 1 ) ;
695+ }
620696}
0 commit comments