1- import fnmatch
2- from itertools import zip_longest
1+ import os
32from pathlib import Path
4- from typing import Dict , List , Optional
3+ from typing import List
4+
5+ from gitignore_parser import parse_gitignore_str
56
67from dstack ._internal .utils .path import PathLike
78
@@ -16,75 +17,83 @@ def __init__(
1617 if ignore_files is not None
1718 else [".gitignore" , ".git/info/exclude" , ".dstackignore" ]
1819 )
19- self .ignore_globs : Dict [ str , List [ str ]] = { "." : globs or []}
20- self .load_recursive ( )
20+ self .parser = None
21+ self ._create_combined_parser ( globs or [] )
2122
22- def load_ignore_file (self , path : str , ignore_file : Path ):
23- if path not in self .ignore_globs :
24- self .ignore_globs [path ] = []
25- with ignore_file .open ("r" ) as f :
26- for line in f :
27- line = self .rstrip (line .rstrip ("\n " )).rstrip ("/" )
28- line = line .replace ("\\ " , " " )
29- if line .startswith ("#" ) or not line :
30- continue
31- self .ignore_globs [path ].append (line )
23+ def _create_combined_parser (self , additional_globs : List [str ]):
24+ """Create a single parser from all ignore files and additional globs."""
25+ all_patterns = []
3226
33- def load_recursive (self , path : Optional [Path ] = None ):
34- path = path or self .root_dir
35- for ignore_file in self .ignore_files :
36- ignore_file = path / ignore_file
37- if ignore_file .exists ():
38- self .load_ignore_file (str (path .relative_to (self .root_dir )), ignore_file )
27+ # Collect patterns from all ignore files recursively
28+ self ._collect_patterns_recursive (self .root_dir , all_patterns )
3929
40- for subdir in path .iterdir ():
41- if not subdir .is_dir () or self .ignore (subdir .relative_to (self .root_dir )):
42- continue
43- self .load_recursive (subdir )
30+ # Add additional glob patterns
31+ all_patterns .extend (additional_globs )
4432
45- @staticmethod
46- def rstrip (value : str ) -> str :
47- end = len (value ) - 1
48- while end >= 0 :
49- if not value [end ].isspace ():
50- break
51- if end > 0 and value [end - 1 ] == "\\ " :
52- break # escaped space
53- end -= 1
54- else :
55- return ""
56- return value [: end + 1 ]
33+ self .parser = parse_gitignore_str ("\n " .join (all_patterns ), self .root_dir )
5734
58- @ staticmethod
59- def fnmatch ( name : str , pattern : str , sep = "/" ) -> bool :
60- if pattern . startswith ( sep ):
61- name = sep + name
62- for n , p in zip_longest (
63- reversed ( name . split ( sep )), reversed ( pattern . split ( sep )), fillvalue = None
64- ):
65- if p == "**" :
66- raise NotImplementedError ()
67- if p is None :
68- return True
69- if n is None or not fnmatch . fnmatch ( n , p ):
70- return False
71- return True
35+ def _collect_patterns_recursive ( self , path : Path , patterns : List [ str ]):
36+ """
37+ Recursively collect patterns from all ignore files and combine them into a single gitignore,
38+ with the root directory as the base path.
39+ """
40+ for ignore_file_name in self . ignore_files :
41+ ignore_file = path / ignore_file_name
42+ if ignore_file . exists () :
43+ try :
44+ # Get relative path from root to this directory
45+ if path == self . root_dir :
46+ prefix = ""
47+ else :
48+ prefix = path . relative_to ( self . root_dir )
7249
73- def ignore (self , path : PathLike , sep = "/" ) -> bool :
74- if not path :
50+ # Read patterns and prefix them with directory path
51+ with ignore_file .open ("r" , encoding = "utf-8" , errors = "ignore" ) as f :
52+ for line in f :
53+ line = line .strip ()
54+ if line and not line .startswith ("#" ):
55+ if prefix :
56+ # Prefix patterns with directory path for subdirectories
57+ if line .startswith ("/" ):
58+ # Absolute pattern within subdirectory
59+ patterns .append (os .path .join (prefix , line [1 :]))
60+ else :
61+ # Relative pattern within subdirectory
62+ # Add pattern that matches files directly in the subdirectory
63+ patterns .append (os .path .join (prefix , line ))
64+ # Add pattern that matches files in deeper subdirectories
65+ patterns .append (os .path .join (prefix , "**" , line ))
66+ else :
67+ # Root directory patterns
68+ patterns .append (line )
69+ except (OSError , UnicodeDecodeError ):
70+ # Skip files we can't read
71+ continue
72+
73+ # Recursively process subdirectories
74+ # Note: We need to check if directories should be ignored, but we can't
75+ # use self.ignore() yet since we're still building the parser
76+ # So we'll process all directories and let gitignore_parser handle the logic
77+ try :
78+ for subdir in path .iterdir ():
79+ if subdir .is_dir ():
80+ self ._collect_patterns_recursive (subdir , patterns )
81+ except (OSError , PermissionError ):
82+ # Skip directories we can't read
83+ pass
84+
85+ def ignore (self , path : PathLike ) -> bool :
86+ """Check if a path should be ignored."""
87+ if not path or not self .parser :
7588 return False
89+
7690 path = Path (path )
7791 if path .is_absolute ():
78- path = path .relative_to (self .root_dir )
92+ try :
93+ path = path .relative_to (self .root_dir )
94+ except ValueError :
95+ return False
7996
80- tokens = ("." + sep + str (path )).split (sep )
81- for i in range (1 , len (tokens )):
82- parent = sep .join (tokens [:- i ])
83- globs = self .ignore_globs .get (parent )
84- if not globs :
85- continue
86- name = sep .join (tokens [- i :])
87- for glob in globs :
88- if self .fnmatch (name , glob , sep = sep ):
89- return True
90- return False
97+ # Convert to absolute path for gitignore_parser
98+ abs_path = str (self .root_dir / path )
99+ return self .parser (abs_path )
0 commit comments