@@ -54,12 +54,36 @@ impl Exclude {
5454 for pattern in & self . urn {
5555 if let Some ( negated) = pattern. strip_prefix ( '!' ) {
5656 // Negation pattern - add to whitelist
57- let glob = Glob :: new ( negated) ?;
57+ // Transform simple filename patterns to match anywhere in the tree
58+ let transformed = if negated. contains ( '/' ) || negated. starts_with ( "**/" ) {
59+ negated. to_string ( )
60+ } else {
61+ // Match the item itself and everything inside it
62+ format ! ( "**/{}" , negated)
63+ } ;
64+ let glob = Glob :: new ( & transformed) ?;
5865 whitelist_builder. add ( glob) ;
66+ // Also match everything inside directories with this name
67+ if !negated. contains ( '/' ) && !negated. starts_with ( "**/" ) {
68+ let glob_inner = Glob :: new ( & format ! ( "**/{}/**" , negated) ) ?;
69+ whitelist_builder. add ( glob_inner) ;
70+ }
5971 } else {
6072 // Regular pattern - add to ignore list
61- let glob = Glob :: new ( pattern) ?;
73+ // Transform simple filename patterns to match anywhere in the tree
74+ let transformed = if pattern. contains ( '/' ) || pattern. starts_with ( "**/" ) {
75+ pattern. to_string ( )
76+ } else {
77+ // Match the item itself: **/.git
78+ format ! ( "**/{}" , pattern)
79+ } ;
80+ let glob = Glob :: new ( & transformed) ?;
6281 ignore_builder. add ( glob) ;
82+ // Also match everything inside directories with this name: **/.git/**
83+ if !pattern. contains ( '/' ) && !pattern. starts_with ( "**/" ) {
84+ let glob_inner = Glob :: new ( & format ! ( "**/{}/**" , pattern) ) ?;
85+ ignore_builder. add ( glob_inner) ;
86+ }
6387 }
6488 }
6589
@@ -77,39 +101,14 @@ impl Exclude {
77101 pub fn matches_path ( & self , path : & Path ) -> Option < bool > {
78102 let compiled = self . compiled . as_ref ( ) ?;
79103
80- // For absolute paths, try both the full path and relative components
81- // This helps patterns like **/.git/** match /home/user/project/.git
82- let paths_to_check: Vec < & Path > = if path. is_absolute ( ) {
83- // Also check each component as if it's relative
84- // This allows **/.git/** to match /home/user/.git/config
85- let mut paths = vec ! [ path] ;
86-
87- // Check if any path component matches by checking relative sub-paths
88- // For /home/user/.git/config, we want to match against .git/config too
89- if let Some ( components) = path. to_str ( ) {
90- for ( i, _) in components. match_indices ( '/' ) . skip ( 1 ) {
91- if let Some ( subpath) = components. get ( i + 1 ..) {
92- paths. push ( Path :: new ( subpath) ) ;
93- }
94- }
95- }
96- paths
97- } else {
98- vec ! [ path]
99- } ;
100-
101104 // Check whitelist first (negation takes precedence)
102- for p in & paths_to_check {
103- if compiled. whitelists . is_match ( p) {
104- return Some ( false ) ; // Explicitly NOT ignored
105- }
105+ if compiled. whitelists . is_match ( path) {
106+ return Some ( false ) ; // Explicitly NOT ignored
106107 }
107108
108109 // Check ignore patterns
109- for p in & paths_to_check {
110- if compiled. ignores . is_match ( p) {
111- return Some ( true ) ; // Should be ignored
112- }
110+ if compiled. ignores . is_match ( path) {
111+ return Some ( true ) ; // Should be ignored
113112 }
114113
115114 None // No match
@@ -127,13 +126,10 @@ impl Exclude {
127126 let pattern = & self . r#in [ 3 ..] ; // Remove leading "**/", e.g., "target" or "target/**"
128127
129128 // Strip trailing /** if present
130- let pattern = if let Some ( p) = pattern. strip_suffix ( "/**" ) {
131- p
132- } else {
133- pattern
134- } ;
129+ let pattern = if let Some ( p) = pattern. strip_suffix ( "/**" ) { p } else { pattern } ;
135130
136- // Check if path ends with the pattern (e.g., /home/user/project/target matches **/target)
131+ // Check if path ends with the pattern (e.g., /home/user/project/target matches
132+ // **/target)
137133 if path. ends_with ( & format ! ( "/{}" , pattern) ) || path. ends_with ( pattern) {
138134 return true ;
139135 }
@@ -174,3 +170,151 @@ impl Exclude {
174170 path == self . r#in || path. starts_with ( & format ! ( "{}/" , self . r#in) )
175171 }
176172}
173+
174+ #[ cfg( test) ]
175+ mod tests {
176+ use std:: path:: Path ;
177+
178+ use super :: * ;
179+
180+ fn create_exclude ( in_pattern : & str , urn_patterns : Vec < & str > ) -> Exclude {
181+ let mut exclude = Exclude {
182+ r#in : in_pattern. to_string ( ) ,
183+ urn : urn_patterns. into_iter ( ) . map ( String :: from) . collect ( ) ,
184+ compiled : None ,
185+ } ;
186+ exclude. compile ( ) . unwrap ( ) ;
187+ exclude
188+ }
189+
190+ #[ test]
191+ fn test_context_matching_with_dots ( ) {
192+ let exclude = create_exclude ( "*" , vec ! [ ".git" ] ) ;
193+
194+ // Test various path contexts - all should match "*"
195+ let test_cases = vec ! [
196+ ( "/home/carlosedp/projects/yazi/yazi" , true ) ,
197+ ( "/home/carlosedp/projects/yazi/yazi-rs.github.io" , true ) ,
198+ ( "/home/carlosedp/projects/yazi/command-palette.yazi" , true ) ,
199+ ( "/home/carlosedp/projects/yazi/gitignore.yazi" , true ) ,
200+ ] ;
201+
202+ for ( context, should_match) in test_cases {
203+ let matches = exclude. matches_context ( context) ;
204+ assert_eq ! (
205+ matches, should_match,
206+ "Context matching failed for {}: expected {}, got {}" ,
207+ context, should_match, matches
208+ ) ;
209+ }
210+ }
211+
212+ #[ test]
213+ fn test_simple_pattern_behavior ( ) {
214+ // Test how globset matches simple patterns like ".git"
215+ use globset:: Glob ;
216+
217+ let patterns_and_paths = vec ! [
218+ // Pattern ".git" gets transformed to "**/.git" in our compile() method
219+ // So let's test the transformed version
220+ ( "**/.git" , "/home/user/.git" , true ) ,
221+ ( "**/.git" , "/home/user/proj/.git" , true ) ,
222+ ( "**/.git" , "/home/user/command-palette.yazi/.git" , true ) ,
223+ ( "**/.git" , ".git" , true ) ,
224+ // **/.git/** matches files INSIDE .git, not the .git directory itself
225+ ( "**/.git/**" , "/home/user/.git" , false ) ,
226+ ( "**/.git/**" , "/home/user/.git/config" , true ) ,
227+ ] ;
228+
229+ for ( pattern, path, expected) in patterns_and_paths {
230+ let glob = Glob :: new ( pattern) . unwrap ( ) . compile_matcher ( ) ;
231+ let matches = glob. is_match ( Path :: new ( path) ) ;
232+ assert_eq ! (
233+ matches, expected,
234+ "Pattern '{}' with path '{}': expected {}, got {}" ,
235+ pattern, path, expected, matches
236+ ) ;
237+ }
238+ }
239+
240+ #[ test]
241+ fn test_path_matching_with_git ( ) {
242+ let exclude = create_exclude ( "*" , vec ! [ ".git" ] ) ;
243+
244+ // Test various .git paths
245+ // Pattern ".git" is transformed to "**/.git" which matches any component named
246+ // .git
247+ let test_paths = vec ! [
248+ ( "/home/carlosedp/projects/yazi/yazi/.git" , true ) ,
249+ ( "/home/carlosedp/projects/yazi/yazi-rs.github.io/.git" , true ) ,
250+ ( "/home/carlosedp/projects/yazi/command-palette.yazi/.git" , true ) ,
251+ ( "/home/carlosedp/projects/yazi/gitignore.yazi/.git" , true ) ,
252+ // Files inside .git now also match "**/.git" since .git is the component name
253+ ( "/home/carlosedp/projects/yazi/yazi/.git/config" , true ) ,
254+ ( "/home/carlosedp/projects/yazi/command-palette.yazi/.git/config" , true ) ,
255+ ] ;
256+
257+ for ( path_str, should_match) in test_paths {
258+ let path = Path :: new ( path_str) ;
259+ let result = exclude. matches_path ( path) ;
260+ // None means no match, which is equivalent to false (not ignored)
261+ let actual = result. unwrap_or ( false ) ;
262+ assert_eq ! (
263+ actual, should_match,
264+ "Path matching failed for {}: expected {}, got {}" ,
265+ path_str, should_match, actual
266+ ) ;
267+ }
268+ }
269+
270+ #[ test]
271+ fn test_glob_pattern_matching ( ) {
272+ // User provides "**/.git" explicitly - matches only things NAMED .git
273+ let exclude = create_exclude ( "*" , vec ! [ "**/.git" ] ) ;
274+
275+ let test_paths = vec ! [
276+ // Should match .git directory itself
277+ ( "/home/carlosedp/projects/yazi/yazi/.git" , true ) ,
278+ ( "/home/carlosedp/projects/yazi/command-palette.yazi/.git" , true ) ,
279+ // Should NOT match files inside .git when using **/.git alone
280+ ( "/home/carlosedp/projects/yazi/yazi/.git/config" , false ) ,
281+ ] ;
282+
283+ for ( path_str, should_match) in test_paths {
284+ let path = Path :: new ( path_str) ;
285+ let result = exclude. matches_path ( path) ;
286+ let actual = result. unwrap_or ( false ) ;
287+ assert_eq ! (
288+ actual, should_match,
289+ "Glob pattern matching failed for {}: expected {}, got {}" ,
290+ path_str, should_match, actual
291+ ) ;
292+ }
293+ }
294+
295+ #[ test]
296+ fn test_glob_pattern_with_trailing_slash ( ) {
297+ // Pattern **/.git/** means match everything inside any .git directory
298+ let exclude = create_exclude ( "*" , vec ! [ "**/.git/**" ] ) ;
299+
300+ let test_paths = vec ! [
301+ // Should NOT match .git directory itself with /**
302+ ( "/home/carlosedp/projects/yazi/yazi/.git" , false ) ,
303+ // Should match all files inside .git
304+ ( "/home/carlosedp/projects/yazi/yazi/.git/config" , true ) ,
305+ ( "/home/carlosedp/projects/yazi/command-palette.yazi/.git" , false ) ,
306+ ( "/home/carlosedp/projects/yazi/command-palette.yazi/.git/config" , true ) ,
307+ ] ;
308+
309+ for ( path_str, should_match) in test_paths {
310+ let path = Path :: new ( path_str) ;
311+ let result = exclude. matches_path ( path) ;
312+ let actual = result. unwrap_or ( false ) ;
313+ assert_eq ! (
314+ actual, should_match,
315+ "Glob pattern with /** matching failed for {}: expected {}, got {}" ,
316+ path_str, should_match, actual
317+ ) ;
318+ }
319+ }
320+ }
0 commit comments