@@ -77,17 +77,39 @@ def installed_distributions_snapshot() -> dict[str, str]:
7777def included_workspace_files (
7878 workspace : Path , exclude_patterns : list [str ]
7979) -> list [Path ]:
80+ """
81+ Return a list of files to be included in the workspace snapshot.
82+ Optimized to skip excluded directories early.
83+ """
8084 included : list [Path ] = []
81- for path in sorted (workspace .rglob ("*" )):
82- if not path .is_file ():
83- continue
84- rel = path .relative_to (workspace ).as_posix ()
85- if rel .startswith (".ark/" ):
86- continue
87- if any (_matches_exclude_pattern (rel , pattern ) for pattern in exclude_patterns ):
88- continue
89- included .append (path )
90- return included
85+ ws_str = str (workspace .resolve ())
86+
87+ # Prune list for os.walk
88+ prune_dirs = {".git" , ".ark" , "__pycache__" , "venv" , ".venv" , "build" , "dist" }
89+
90+ import os
91+ for root , dirs , files in os .walk (ws_str ):
92+ # 1. Early pruning of common heavy/system directories
93+ dirs [:] = [d for d in dirs if d not in prune_dirs ]
94+
95+ # 2. Apply custom exclude patterns to directories
96+ rel_root = os .path .relpath (root , ws_str )
97+ if rel_root == "." :
98+ rel_root = ""
99+
100+ if rel_root :
101+ if any (_matches_exclude_pattern (rel_root + "/" , p ) for p in exclude_patterns ):
102+ dirs [:] = [] # Stop recursion here
103+ continue
104+
105+ # 3. Process files
106+ for f in files :
107+ rel_path = os .path .join (rel_root , f ) if rel_root else f
108+ if any (_matches_exclude_pattern (rel_path , p ) for p in exclude_patterns ):
109+ continue
110+ included .append (Path (root ) / f )
111+
112+ return sorted (included )
91113
92114
93115def _matches_exclude_pattern (relative_path : str , pattern : str ) -> bool :
@@ -106,14 +128,21 @@ def _matches_exclude_pattern(relative_path: str, pattern: str) -> bool:
106128
107129
108130def compute_workspace_hash (workspace : Path , exclude_patterns : list [str ]) -> str :
131+ """
132+ Compute a fast hash of the workspace using file metadata (path, size, mtime).
133+ This is much faster than reading full content for large projects.
134+ """
109135 digest = sha256 ()
110136 for path in included_workspace_files (workspace , exclude_patterns ):
111- rel = path .relative_to (workspace ).as_posix ().encode ("utf-8" )
112- digest .update (rel )
113- digest .update (b"\0 " )
114- digest .update (path .read_bytes ())
115- digest .update (b"\0 " )
116- return "sha256:" + digest .hexdigest ()
137+ try :
138+ stat = path .stat ()
139+ # We hash the relative path, size, and mtime
140+ rel = path .relative_to (workspace ).as_posix ()
141+ entry = f"{ rel } |{ stat .st_size } |{ stat .st_mtime } "
142+ digest .update (entry .encode ("utf-8" ))
143+ except Exception :
144+ continue
145+ return "metadata-sha256:" + digest .hexdigest ()
117146
118147
119148def get_git_commit_hash (workspace : Path ) -> str | None :
0 commit comments