11#![ deny( clippy:: all) ]
22
3- use std:: { borrow:: Cow , collections:: HashMap , path:: Path , path:: PathBuf } ;
3+ use std:: { borrow:: Cow , collections:: HashMap , path:: Path , path:: PathBuf , sync :: Arc } ;
44
55use ignore:: {
66 gitignore:: { Gitignore , GitignoreBuilder } ,
@@ -138,6 +138,7 @@ impl IgnoreMatcher {
138138/// A hierarchy of repository `.gitignore` files keyed by their root-relative directories.
139139#[ derive( Default ) ]
140140pub struct GitIgnoreMatcher {
141+ compiled_patterns : HashMap < Box < str > , Arc < Gitignore > > ,
141142 matchers : HashMap < Box < str > , GitIgnoreSourceMatcher > ,
142143 // Traversal checks every file's parent, so cache both ignored and included directories.
143144 ignored_directories : HashMap < Box < str > , bool > ,
@@ -149,6 +150,11 @@ impl GitIgnoreMatcher {
149150 Self :: default ( )
150151 }
151152
153+ /// Releases compiled rules and matching state after directory traversal.
154+ pub fn clear ( & mut self ) {
155+ * self = Self :: default ( ) ;
156+ }
157+
152158 /// Adds or replaces the patterns rooted at a POSIX, repository-relative directory.
153159 ///
154160 /// Returns whether the hierarchy contains any effective rules after the update.
@@ -158,7 +164,15 @@ impl GitIgnoreMatcher {
158164 patterns : & str ,
159165 ) -> Result < bool , ignore:: Error > {
160166 let relative_root = normalize_relative_path ( relative_root) ;
161- let matcher = compile_patterns ( patterns) ?;
167+ // Compiled rules use relative paths, so identical contents can share them across roots.
168+ let matcher = if let Some ( matcher) = self . compiled_patterns . get ( patterns) {
169+ Arc :: clone ( matcher)
170+ } else {
171+ let matcher = Arc :: new ( compile_patterns ( patterns) ?) ;
172+ self . compiled_patterns
173+ . insert ( patterns. into ( ) , Arc :: clone ( & matcher) ) ;
174+ matcher
175+ } ;
162176
163177 self . invalidate_directory_cache ( relative_root) ;
164178 if matcher. is_empty ( ) {
@@ -329,12 +343,12 @@ impl GitIgnoreMatcher {
329343/// One `.gitignore` source with the directory state needed to reproduce
330344/// `ignore.test(path)` without re-walking ancestors for every file.
331345struct GitIgnoreSourceMatcher {
332- matcher : Gitignore ,
346+ matcher : Arc < Gitignore > ,
333347 directory_states : HashMap < Box < str > , Option < bool > > ,
334348}
335349
336350impl GitIgnoreSourceMatcher {
337- fn new ( matcher : Gitignore ) -> Self {
351+ fn new ( matcher : Arc < Gitignore > ) -> Self {
338352 Self {
339353 matcher,
340354 directory_states : HashMap :: new ( ) ,
@@ -550,6 +564,31 @@ mod tests {
550564 use super :: { GitIgnoreMatcher , IgnoreMatcher , IgnoreSource } ;
551565 use std:: path:: Path ;
552566
567+ #[ test]
568+ fn identical_gitignore_contents_keep_independent_roots ( ) {
569+ let mut matcher = GitIgnoreMatcher :: new ( ) ;
570+ let patterns = "/dist/\n " ;
571+ matcher. add_source ( "a" , patterns) . unwrap ( ) ;
572+ matcher. add_source ( "b" , patterns) . unwrap ( ) ;
573+ for root in [ "a" , "b" ] {
574+ assert ! ( matcher. is_ignored( & format!( "{root}/dist/index.js" ) , false ) ) ;
575+ assert ! ( !matcher. is_ignored( & format!( "{root}/nested/dist/index.js" ) , false ) ) ;
576+ }
577+ matcher. add_source ( "a" , "" ) . unwrap ( ) ;
578+ assert ! ( !matcher. is_ignored( "a/dist/index.js" , false ) ) ;
579+ assert ! ( matcher. is_ignored( "b/dist/index.js" , false ) ) ;
580+ assert ! ( !matcher. is_ignored( "dist/index.js" , false ) ) ;
581+ }
582+
583+ #[ test]
584+ fn clearing_gitignore_removes_rules_and_directory_state ( ) {
585+ let mut matcher = GitIgnoreMatcher :: new ( ) ;
586+ matcher. add_source ( "" , "dist/\n " ) . unwrap ( ) ;
587+ assert ! ( matcher. is_ignored( "dist/index.js" , false ) ) ;
588+ matcher. clear ( ) ;
589+ assert ! ( !matcher. is_ignored( "dist/index.js" , false ) ) ;
590+ }
591+
553592 #[ test]
554593 fn keeps_independent_ignore_sources_isolated ( ) {
555594 let mut matcher = IgnoreMatcher :: new ( [
0 commit comments