2323from .._utils ._project_files import ( # type: ignore
2424 FileInfo ,
2525 FileOperationUpdate ,
26+ InteractiveConflictHandler ,
27+ compute_normalized_hash ,
2628 files_to_include ,
2729 read_toml_project ,
2830)
@@ -56,19 +58,24 @@ def __init__(
5658 project_id : str ,
5759 directory : str ,
5860 include_uv_lock : bool = True ,
61+ conflict_handler : Optional [InteractiveConflictHandler ] = None ,
5962 ) -> None :
6063 """Initialize the SwFileHandler.
6164
6265 Args:
6366 project_id: The ID of the UiPath project
6467 directory: Local project directory
6568 include_uv_lock: Whether to include uv.lock file
69+ conflict_handler: Optional handler for file conflicts
6670 """
6771 self .directory = directory
6872 self .include_uv_lock = include_uv_lock
6973 self .console = ConsoleLogger ()
7074 self ._studio_client = StudioClient (project_id )
7175 self ._project_structure : Optional [ProjectStructure ] = None
76+ self ._conflict_handler = conflict_handler or InteractiveConflictHandler (
77+ operation = "push"
78+ )
7279
7380 def _get_folder_by_name (
7481 self , structure : ProjectStructure , folder_name : str
@@ -186,20 +193,78 @@ async def _process_file_uploads(
186193 )
187194
188195 if remote_file :
189- # File exists remotely - mark for update
196+ # File exists remotely - check if content differs
190197 processed_source_files .add (remote_file .id )
191- structural_migration .modified_resources .append (
192- ModifiedResource (
193- id = remote_file .id , content_file_path = local_file .file_path
198+
199+ # Download remote file and compare with local
200+ try :
201+ remote_response = (
202+ await self ._studio_client .download_project_file_async (
203+ remote_file
204+ )
194205 )
195- )
196- updates .append (
197- FileOperationUpdate (
198- file_path = local_file .file_path ,
199- status = "updating" ,
200- message = f"Updating '{ local_file .file_name } '" ,
206+ remote_content = remote_response .read ().decode ("utf-8" )
207+ remote_hash = compute_normalized_hash (remote_content )
208+
209+ with open (local_file .file_path , "r" , encoding = "utf-8" ) as f :
210+ local_content = f .read ()
211+ local_hash = compute_normalized_hash (local_content )
212+
213+ # Only update if content differs and user confirms
214+ if local_hash != remote_hash :
215+ if self ._conflict_handler .should_overwrite (
216+ local_file .relative_path ,
217+ remote_hash ,
218+ local_hash ,
219+ local_full_path = os .path .abspath (local_file .file_path ),
220+ ):
221+ structural_migration .modified_resources .append (
222+ ModifiedResource (
223+ id = remote_file .id ,
224+ content_file_path = local_file .file_path ,
225+ )
226+ )
227+ updates .append (
228+ FileOperationUpdate (
229+ file_path = local_file .file_path ,
230+ status = "updating" ,
231+ message = f"Updating '{ local_file .file_name } '" ,
232+ )
233+ )
234+ else :
235+ updates .append (
236+ FileOperationUpdate (
237+ file_path = local_file .file_path ,
238+ status = "skipped" ,
239+ message = f"Skipped '{ local_file .file_name } '" ,
240+ )
241+ )
242+ else :
243+ # Content is the same, no need to update
244+ updates .append (
245+ FileOperationUpdate (
246+ file_path = local_file .file_path ,
247+ status = "up_to_date" ,
248+ message = f"File '{ local_file .file_name } ' is up to date" ,
249+ )
250+ )
251+ except Exception as e :
252+ logger .warning (
253+ f"Failed to compare file '{ local_file .file_path } ': { e } "
254+ )
255+ # If comparison fails, proceed with update
256+ structural_migration .modified_resources .append (
257+ ModifiedResource (
258+ id = remote_file .id , content_file_path = local_file .file_path
259+ )
260+ )
261+ updates .append (
262+ FileOperationUpdate (
263+ file_path = local_file .file_path ,
264+ status = "updating" ,
265+ message = f"Updating '{ local_file .file_name } '" ,
266+ )
201267 )
202- )
203268 else :
204269 # File doesn't exist remotely - mark for upload
205270 parent_path = os .path .dirname (local_file .relative_path )
@@ -741,7 +806,7 @@ def _collect_files_from_folder(
741806 files [file .name ] = file
742807 return files
743808
744- def _process_file_sync (
809+ async def _process_file_sync (
745810 self ,
746811 local_file_path : str ,
747812 remote_files : Dict [str , ProjectFile ],
@@ -766,10 +831,51 @@ def _process_file_sync(
766831
767832 if remote_file :
768833 processed_ids .add (remote_file .id )
769- structural_migration .modified_resources .append (
770- ModifiedResource (id = remote_file .id , content_file_path = local_file_path )
771- )
772- self .console .info (f"Updating { click .style (destination , fg = 'yellow' )} " )
834+
835+ # Download remote file and compare with local
836+ try :
837+ remote_response = await self ._studio_client .download_project_file_async (
838+ remote_file
839+ )
840+ remote_content = remote_response .read ().decode ("utf-8" )
841+ remote_hash = compute_normalized_hash (remote_content )
842+
843+ with open (local_file_path , "r" , encoding = "utf-8" ) as f :
844+ local_content = f .read ()
845+ local_hash = compute_normalized_hash (local_content )
846+
847+ # Only update if content differs and user confirms
848+ if local_hash != remote_hash :
849+ if self ._conflict_handler .should_overwrite (
850+ destination ,
851+ remote_hash ,
852+ local_hash ,
853+ local_full_path = os .path .abspath (local_file_path ),
854+ ):
855+ structural_migration .modified_resources .append (
856+ ModifiedResource (
857+ id = remote_file .id , content_file_path = local_file_path
858+ )
859+ )
860+ self .console .info (
861+ f"Updating { click .style (destination , fg = 'yellow' )} "
862+ )
863+ else :
864+ self .console .info (
865+ f"Skipped { click .style (destination , fg = 'bright_black' )} "
866+ )
867+ else :
868+ # Content is the same, no need to update
869+ self .console .info (f"File '{ destination } ' is up to date" )
870+ except Exception as e :
871+ logger .warning (f"Failed to compare file '{ local_file_path } ': { e } " )
872+ # If comparison fails, proceed with update
873+ structural_migration .modified_resources .append (
874+ ModifiedResource (
875+ id = remote_file .id , content_file_path = local_file_path
876+ )
877+ )
878+ self .console .info (f"Updating { click .style (destination , fg = 'yellow' )} " )
773879 else :
774880 structural_migration .added_resources .append (
775881 AddedResource (
@@ -870,7 +976,7 @@ async def upload_coded_evals_files(self) -> None:
870976 register_evaluator (evaluator .custom_evaluator_file_name )
871977 )
872978
873- self ._process_file_sync (
979+ await self ._process_file_sync (
874980 evaluator_schema_file_path ,
875981 remote_custom_evaluator_files ,
876982 "coded-evals/evaluators/custom" ,
@@ -879,7 +985,7 @@ async def upload_coded_evals_files(self) -> None:
879985 processed_custom_evaluator_ids ,
880986 )
881987
882- self ._process_file_sync (
988+ await self ._process_file_sync (
883989 evaluator_types_file_path ,
884990 remote_custom_evaluator_type_files ,
885991 "coded-evals/evaluators/custom/types" ,
@@ -888,7 +994,7 @@ async def upload_coded_evals_files(self) -> None:
888994 processed_evaluator_type_ids ,
889995 )
890996
891- self ._process_file_sync (
997+ await self ._process_file_sync (
892998 evaluator .path ,
893999 remote_evaluator_files ,
8941000 "coded-evals/evaluators" ,
@@ -898,7 +1004,7 @@ async def upload_coded_evals_files(self) -> None:
8981004 )
8991005
9001006 for eval_set_file in eval_set_files :
901- self ._process_file_sync (
1007+ await self ._process_file_sync (
9021008 eval_set_file ,
9031009 remote_eval_set_files ,
9041010 "coded-evals/eval-sets" ,
0 commit comments