@@ -2,7 +2,10 @@ use globset::{Glob, GlobSet, GlobSetBuilder};
22
33#[ derive( Clone ) ]
44pub struct ProjectFilter {
5- matcher : GlobSet ,
5+ /// Original patterns (preserves glob syntax like `[a-z]`)
6+ raw_matcher : GlobSet ,
7+ /// Normalized patterns (lowercase, hyphens→underscores for ProjectId matching)
8+ normalized_matcher : GlobSet ,
69}
710
811impl ProjectFilter {
@@ -11,7 +14,7 @@ impl ProjectFilter {
1114 }
1215
1316 pub fn is_excluded ( & self , project : & str ) -> bool {
14- self . matcher . is_match ( project)
17+ self . raw_matcher . is_match ( project ) || self . normalized_matcher . is_match ( project)
1518 }
1619
1720 fn from_env_value ( raw : Option < & str > ) -> Option < Self > {
@@ -23,17 +26,23 @@ impl ProjectFilter {
2326 where
2427 I : IntoIterator < Item = & ' a str > ,
2528 {
26- let mut builder = GlobSetBuilder :: new ( ) ;
29+ let mut raw_builder = GlobSetBuilder :: new ( ) ;
30+ let mut norm_builder = GlobSetBuilder :: new ( ) ;
2731 let mut added = 0usize ;
2832
2933 for pattern in patterns {
3034 let expanded = expand_home ( pattern) ;
31- // Normalize pattern the same way ProjectId::normalize() does
32- // (lowercase, hyphens → underscores) so that a pattern like
33- // "My-Secret-Project" matches the normalized input "my_secret_project".
35+
36+ if let Ok ( glob) = Glob :: new ( & expanded) {
37+ raw_builder. add ( glob) ;
38+ }
39+
40+ // Normalized pattern — lowercase + hyphens→underscores for ProjectId matching.
41+ // This may corrupt character classes ([a-z] → [a_z]) but that's fine:
42+ // the raw matcher already handles those correctly.
3443 let normalized = expanded. to_lowercase ( ) . replace ( '-' , "_" ) ;
3544 if let Ok ( glob) = Glob :: new ( & normalized) {
36- builder . add ( glob) ;
45+ norm_builder . add ( glob) ;
3746 added = added. saturating_add ( 1 ) ;
3847 }
3948 }
@@ -42,8 +51,12 @@ impl ProjectFilter {
4251 return None ;
4352 }
4453
45- let matcher = builder. build ( ) . ok ( ) ?;
46- Some ( Self { matcher } )
54+ let raw_matcher = raw_builder. build ( ) . ok ( ) ?;
55+ let normalized_matcher = norm_builder. build ( ) . ok ( ) ?;
56+ Some ( Self {
57+ raw_matcher,
58+ normalized_matcher,
59+ } )
4760 }
4861}
4962
@@ -89,9 +102,10 @@ mod tests {
89102 #[ test]
90103 fn normalizes_patterns_to_match_project_id ( ) {
91104 let filter = ProjectFilter :: from_env_value ( Some ( "My-Secret-Project" ) ) . expect ( "filter" ) ;
92- // ProjectId::new("My-Secret-Project").to_string() == " my_secret_project"
105+ // Normalized matcher: my_secret_project matches
93106 assert ! ( filter. is_excluded( "my_secret_project" ) ) ;
94- assert ! ( !filter. is_excluded( "My-Secret-Project" ) ) ;
107+ // Raw matcher: exact case match
108+ assert ! ( filter. is_excluded( "My-Secret-Project" ) ) ;
95109 }
96110
97111 #[ test]
@@ -101,6 +115,15 @@ mod tests {
101115 assert ! ( filter. is_excluded( "my_secret_other" ) ) ;
102116 }
103117
118+ #[ test]
119+ fn preserves_glob_character_classes ( ) {
120+ // [a-z] must NOT become [a_z] — raw matcher preserves original syntax
121+ let filter = ProjectFilter :: from_env_value ( Some ( "[a-z]*_project" ) ) . expect ( "filter" ) ;
122+ assert ! ( filter. is_excluded( "my_project" ) ) ;
123+ assert ! ( filter. is_excluded( "x_project" ) ) ;
124+ assert ! ( !filter. is_excluded( "1_project" ) ) ;
125+ }
126+
104127 #[ test]
105128 fn returns_none_for_empty_env_value ( ) {
106129 assert ! ( ProjectFilter :: from_env_value( Some ( " , " ) ) . is_none( ) ) ;
0 commit comments