1use crate::workspace::core::{EntityId, Workspace};
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use serde_json;
10use serde_yaml;
11use std::collections::HashMap;
12use std::path::Path;
13use tokio::fs;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct SyncConfig {
18 pub enabled: bool,
20 pub provider: SyncProvider,
22 pub interval_seconds: u64,
24 pub conflict_strategy: ConflictResolutionStrategy,
26 pub auto_commit: bool,
28 pub auto_push: bool,
30 pub directory_structure: SyncDirectoryStructure,
32 pub sync_direction: SyncDirection,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub enum SyncProvider {
39 Git {
41 repo_url: String,
43 branch: String,
45 auth_token: Option<String>,
47 },
48 Cloud {
50 service_url: String,
52 api_key: String,
54 project_id: String,
56 },
57 Local {
59 directory_path: String,
61 watch_changes: bool,
63 },
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68pub enum ConflictResolutionStrategy {
69 LocalWins,
71 RemoteWins,
73 Manual,
75 LastModified,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81pub enum SyncDirectoryStructure {
82 SingleDirectory,
84 PerWorkspace,
86 Hierarchical,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
92pub enum SyncDirection {
93 Bidirectional,
95 LocalToRemote,
97 RemoteToLocal,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct SyncStatus {
104 pub last_sync: Option<DateTime<Utc>>,
106 pub state: SyncState,
108 pub pending_changes: usize,
110 pub conflicts: usize,
112 pub last_error: Option<String>,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
118pub enum SyncState {
119 NotSynced,
121 Syncing,
123 Synced,
125 SyncFailed,
127 HasConflicts,
129}
130
131#[derive(Debug, Clone)]
133pub struct SyncResult {
134 pub success: bool,
136 pub changes_count: usize,
138 pub conflicts: Vec<SyncConflict>,
140 pub error: Option<String>,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct SyncConflict {
147 pub entity_id: EntityId,
149 pub entity_type: String,
151 pub local_version: serde_json::Value,
153 pub remote_version: serde_json::Value,
155 pub resolution: ConflictResolution,
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize)]
161pub enum ConflictResolution {
162 Local,
164 Remote,
166 Manual,
168}
169
170#[derive(Debug, Clone)]
172pub struct WorkspaceSyncManager {
173 config: SyncConfig,
175 status: SyncStatus,
177 conflicts: Vec<SyncConflict>,
179 total_syncs: usize,
181 successful_syncs: usize,
183 failed_syncs: usize,
185 resolved_conflicts: usize,
187 last_sync_duration_ms: Option<u64>,
189}
190
191#[derive(Debug, Clone)]
193pub enum SyncEvent {
194 Started,
196 Progress {
198 current: usize,
200 total: usize,
202 },
203 Completed(SyncResult),
205 Failed(String),
207 ConflictDetected(SyncConflict),
209}
210
211impl WorkspaceSyncManager {
212 pub fn new(config: SyncConfig) -> Self {
214 let status = SyncStatus {
215 last_sync: None,
216 state: SyncState::NotSynced,
217 pending_changes: 0,
218 conflicts: 0,
219 last_error: None,
220 };
221
222 Self {
223 config,
224 status,
225 conflicts: Vec::new(),
226 total_syncs: 0,
227 successful_syncs: 0,
228 failed_syncs: 0,
229 resolved_conflicts: 0,
230 last_sync_duration_ms: None,
231 }
232 }
233
234 pub fn get_config(&self) -> &SyncConfig {
236 &self.config
237 }
238
239 pub fn update_config(&mut self, config: SyncConfig) {
241 self.config = config;
242 }
243
244 pub fn get_status(&self) -> &SyncStatus {
246 &self.status
247 }
248
249 pub fn get_conflicts(&self) -> &[SyncConflict] {
251 &self.conflicts
252 }
253
254 pub fn is_enabled(&self) -> bool {
256 self.config.enabled
257 }
258
259 pub async fn sync_workspace(
261 &mut self,
262 workspace: &mut Workspace,
263 ) -> Result<SyncResult, String> {
264 if !self.config.enabled {
265 return Err("Synchronization is disabled".to_string());
266 }
267
268 self.total_syncs += 1;
270
271 let start_time = std::time::Instant::now();
273
274 self.status.state = SyncState::Syncing;
275 self.status.last_error = None;
276
277 let result = match &self.config.provider {
278 SyncProvider::Git {
279 repo_url,
280 branch,
281 auth_token,
282 } => self.sync_with_git(workspace, repo_url, branch, auth_token.as_deref()).await,
283 SyncProvider::Cloud {
284 service_url,
285 api_key,
286 project_id,
287 } => self.sync_with_cloud(workspace, service_url, api_key, project_id).await,
288 SyncProvider::Local {
289 directory_path,
290 watch_changes,
291 } => self.sync_with_local(workspace, directory_path, *watch_changes).await,
292 };
293
294 let duration = start_time.elapsed();
296 let duration_ms = duration.as_millis() as u64;
297 self.last_sync_duration_ms = Some(duration_ms);
298
299 match &result {
300 Ok(sync_result) => {
301 if sync_result.success {
302 self.successful_syncs += 1;
303 self.status.state = SyncState::Synced;
304 self.status.last_sync = Some(Utc::now());
305 self.status.pending_changes = 0;
306 self.status.conflicts = sync_result.conflicts.len();
307 } else {
308 self.failed_syncs += 1;
309 self.status.state = SyncState::SyncFailed;
310 self.status.last_error = sync_result.error.clone();
311 }
312 }
313 Err(error) => {
314 self.failed_syncs += 1;
315 self.status.state = SyncState::SyncFailed;
316 self.status.last_error = Some(error.clone());
317 }
318 }
319
320 result
321 }
322
323 async fn sync_with_git(
325 &self,
326 workspace: &mut Workspace,
327 repo_url: &str,
328 branch: &str,
329 auth_token: Option<&str>,
330 ) -> Result<SyncResult, String> {
331 let temp_dir =
333 tempfile::tempdir().map_err(|e| format!("Failed to create temp directory: {}", e))?;
334
335 let repo_path = temp_dir.path().join("repo");
336
337 match self.config.sync_direction {
338 SyncDirection::LocalToRemote => {
339 self.sync_local_to_git(workspace, repo_url, branch, auth_token, &repo_path)
340 .await
341 }
342 SyncDirection::RemoteToLocal => {
343 self.sync_git_to_local(workspace, repo_url, branch, auth_token, &repo_path)
344 .await
345 }
346 SyncDirection::Bidirectional => {
347 self.sync_bidirectional_git(workspace, repo_url, branch, auth_token, &repo_path)
348 .await
349 }
350 }
351 }
352
353 async fn sync_local_to_git(
355 &self,
356 workspace: &Workspace,
357 repo_url: &str,
358 branch: &str,
359 auth_token: Option<&str>,
360 repo_path: &std::path::Path,
361 ) -> Result<SyncResult, String> {
362 self.ensure_git_repo(repo_url, branch, auth_token, repo_path).await?;
364
365 let workspace_file = repo_path.join(format!("{}.yaml", workspace.id));
367 let workspace_yaml = serde_yaml::to_string(workspace)
368 .map_err(|e| format!("Failed to serialize workspace: {}", e))?;
369
370 tokio::fs::write(&workspace_file, &workspace_yaml)
371 .await
372 .map_err(|e| format!("Failed to write workspace file: {}", e))?;
373
374 self.git_add_commit_push(repo_path, &workspace_file, auth_token).await?;
376
377 Ok(SyncResult {
378 success: true,
379 changes_count: 1,
380 conflicts: vec![],
381 error: None,
382 })
383 }
384
385 async fn sync_git_to_local(
387 &self,
388 workspace: &mut Workspace,
389 repo_url: &str,
390 branch: &str,
391 auth_token: Option<&str>,
392 repo_path: &std::path::Path,
393 ) -> Result<SyncResult, String> {
394 self.ensure_git_repo(repo_url, branch, auth_token, repo_path).await?;
396
397 let workspace_file = repo_path.join(format!("{}.yaml", workspace.id));
399
400 if !workspace_file.exists() {
401 return Ok(SyncResult {
402 success: true,
403 changes_count: 0,
404 conflicts: vec![],
405 error: None,
406 });
407 }
408
409 let workspace_yaml = tokio::fs::read_to_string(&workspace_file)
410 .await
411 .map_err(|e| format!("Failed to read workspace file: {}", e))?;
412
413 let remote_workspace: Workspace = serde_yaml::from_str(&workspace_yaml)
414 .map_err(|e| format!("Failed to deserialize workspace: {}", e))?;
415
416 let conflicts = self.detect_conflicts(workspace, &remote_workspace);
418
419 if conflicts.is_empty() {
420 *workspace = remote_workspace;
422 Ok(SyncResult {
423 success: true,
424 changes_count: 1,
425 conflicts: vec![],
426 error: None,
427 })
428 } else {
429 Ok(SyncResult {
431 success: true,
432 changes_count: 0,
433 conflicts,
434 error: None,
435 })
436 }
437 }
438
439 async fn sync_bidirectional_git(
441 &self,
442 workspace: &mut Workspace,
443 repo_url: &str,
444 branch: &str,
445 auth_token: Option<&str>,
446 repo_path: &std::path::Path,
447 ) -> Result<SyncResult, String> {
448 let pull_result = self
450 .sync_git_to_local(workspace, repo_url, branch, auth_token, repo_path)
451 .await?;
452
453 if !pull_result.conflicts.is_empty() {
454 return Ok(pull_result);
456 }
457
458 self.sync_local_to_git(workspace, repo_url, branch, auth_token, repo_path).await
460 }
461
462 async fn ensure_git_repo(
464 &self,
465 repo_url: &str,
466 branch: &str,
467 auth_token: Option<&str>,
468 repo_path: &std::path::Path,
469 ) -> Result<(), String> {
470 use std::process::Command;
471
472 let repo_path_str = repo_path.to_string_lossy();
474
475 if repo_path.exists() {
476 let output = Command::new("git")
478 .args(["-C", repo_path_str.as_ref(), "pull", "origin", branch])
479 .output()
480 .map_err(|e| format!("Failed to pull repository: {}", e))?;
481
482 if !output.status.success() {
483 let stderr = String::from_utf8_lossy(&output.stderr);
484 return Err(format!("Git pull failed: {}", stderr));
485 }
486 } else {
487 let clone_url = if let Some(token) = auth_token {
490 self.inject_auth_token_into_url(repo_url, token)
491 } else {
492 repo_url.to_string()
493 };
494
495 let output = Command::new("git")
496 .args([
497 "clone",
498 "--branch",
499 branch,
500 &clone_url,
501 repo_path_str.as_ref(),
502 ])
503 .output()
504 .map_err(|e| format!("Failed to clone repository: {}", e))?;
505
506 if !output.status.success() {
507 let stderr = String::from_utf8_lossy(&output.stderr);
508 return Err(format!("Git clone failed: {}", stderr));
509 }
510 }
511
512 Ok(())
513 }
514
515 async fn git_add_commit_push(
517 &self,
518 repo_path: &std::path::Path,
519 workspace_file: &std::path::Path,
520 _auth_token: Option<&str>,
521 ) -> Result<(), String> {
522 use std::process::Command;
523
524 let repo_path_str = repo_path.to_string_lossy();
526
527 let file_path_str = workspace_file
529 .strip_prefix(repo_path)
530 .unwrap_or(workspace_file)
531 .to_string_lossy();
532
533 let output = Command::new("git")
535 .args(["-C", repo_path_str.as_ref(), "add", file_path_str.as_ref()])
536 .output()
537 .map_err(|e| format!("Failed to add file to git: {}", e))?;
538
539 if !output.status.success() {
540 let stderr = String::from_utf8_lossy(&output.stderr);
541 return Err(format!("Git add failed: {}", stderr));
542 }
543
544 let status_output = Command::new("git")
546 .args(["-C", repo_path_str.as_ref(), "status", "--porcelain"])
547 .output()
548 .map_err(|e| format!("Failed to check git status: {}", e))?;
549
550 if status_output.stdout.is_empty() {
551 return Ok(());
553 }
554
555 let output = Command::new("git")
557 .args([
558 "-C",
559 repo_path_str.as_ref(),
560 "commit",
561 "-m",
562 "Update workspace",
563 ])
564 .output()
565 .map_err(|e| format!("Failed to commit changes: {}", e))?;
566
567 if !output.status.success() {
568 let stderr = String::from_utf8_lossy(&output.stderr);
569 return Err(format!("Git commit failed: {}", stderr));
570 }
571
572 let output = Command::new("git")
574 .args(["-C", repo_path_str.as_ref(), "push", "origin", "HEAD"])
575 .output()
576 .map_err(|e| format!("Failed to push changes: {}", e))?;
577
578 if !output.status.success() {
579 let stderr = String::from_utf8_lossy(&output.stderr);
580 return Err(format!("Git push failed: {}", stderr));
581 }
582
583 Ok(())
584 }
585
586 fn inject_auth_token_into_url(&self, url: &str, token: &str) -> String {
588 if let Some(https_pos) = url.find("https://") {
589 let rest = &url[https_pos + "https://".len()..];
590 format!("https://oauth2:{}@{}", token, rest)
591 } else {
592 url.to_string()
594 }
595 }
596
597 async fn sync_with_cloud(
599 &self,
600 workspace: &mut Workspace,
601 service_url: &str,
602 api_key: &str,
603 project_id: &str,
604 ) -> Result<SyncResult, String> {
605 let client = reqwest::Client::new();
607
608 let base_url = service_url.trim_end_matches('/');
610 let workspace_url =
611 format!("{}/api/v1/projects/{}/workspaces/{}", base_url, project_id, workspace.id);
612
613 match self.config.sync_direction {
614 SyncDirection::LocalToRemote => {
615 self.upload_workspace_to_cloud(&client, &workspace_url, api_key, workspace)
617 .await
618 }
619 SyncDirection::RemoteToLocal => {
620 self.download_workspace_from_cloud(&client, &workspace_url, api_key, workspace)
622 .await
623 }
624 SyncDirection::Bidirectional => {
625 self.bidirectional_sync(&client, &workspace_url, api_key, workspace).await
627 }
628 }
629 }
630
631 async fn upload_workspace_to_cloud(
633 &self,
634 client: &reqwest::Client,
635 workspace_url: &str,
636 api_key: &str,
637 workspace: &Workspace,
638 ) -> Result<SyncResult, String> {
639 let workspace_json = serde_json::to_string(workspace)
641 .map_err(|e| format!("Failed to serialize workspace: {}", e))?;
642
643 let response = client
645 .put(workspace_url)
646 .header("Authorization", format!("Bearer {}", api_key))
647 .header("Content-Type", "application/json")
648 .body(workspace_json)
649 .send()
650 .await
651 .map_err(|e| format!("Failed to upload workspace: {}", e))?;
652
653 if !response.status().is_success() {
654 let status = response.status();
655 let error_text = response.text().await.unwrap_or_default();
656 return Err(format!("Cloud upload failed with status {}: {}", status, error_text));
657 }
658
659 Ok(SyncResult {
660 success: true,
661 changes_count: 1,
662 conflicts: vec![],
663 error: None,
664 })
665 }
666
667 async fn download_workspace_from_cloud(
669 &self,
670 client: &reqwest::Client,
671 workspace_url: &str,
672 api_key: &str,
673 workspace: &mut Workspace,
674 ) -> Result<SyncResult, String> {
675 let response = client
677 .get(workspace_url)
678 .header("Authorization", format!("Bearer {}", api_key))
679 .send()
680 .await
681 .map_err(|e| format!("Failed to download workspace: {}", e))?;
682
683 if response.status() == reqwest::StatusCode::NOT_FOUND {
684 return Ok(SyncResult {
686 success: true,
687 changes_count: 0,
688 conflicts: vec![],
689 error: None,
690 });
691 }
692
693 if !response.status().is_success() {
694 let status = response.status();
695 let error_text = response.text().await.unwrap_or_default();
696 return Err(format!("Cloud download failed with status {}: {}", status, error_text));
697 }
698
699 let remote_json: serde_json::Value = response
700 .json()
701 .await
702 .map_err(|e| format!("Failed to parse remote workspace: {}", e))?;
703
704 let remote_workspace: Workspace = serde_json::from_value(remote_json.clone())
706 .map_err(|e| format!("Failed to deserialize remote workspace: {}", e))?;
707
708 let conflicts = self.detect_conflicts(workspace, &remote_workspace);
710
711 if conflicts.is_empty() {
713 *workspace = remote_workspace;
715 Ok(SyncResult {
716 success: true,
717 changes_count: 1,
718 conflicts: vec![],
719 error: None,
720 })
721 } else {
722 Ok(SyncResult {
724 success: true,
725 changes_count: 0,
726 conflicts,
727 error: None,
728 })
729 }
730 }
731
732 async fn bidirectional_sync(
734 &self,
735 client: &reqwest::Client,
736 workspace_url: &str,
737 api_key: &str,
738 workspace: &mut Workspace,
739 ) -> Result<SyncResult, String> {
740 let download_result = self
742 .download_workspace_from_cloud(client, workspace_url, api_key, workspace)
743 .await?;
744
745 if !download_result.conflicts.is_empty() {
746 return Ok(download_result);
748 }
749
750 self.upload_workspace_to_cloud(client, workspace_url, api_key, workspace).await
752 }
753
754 fn detect_conflicts(&self, local: &Workspace, remote: &Workspace) -> Vec<SyncConflict> {
756 let mut conflicts = vec![];
757
758 if local.updated_at > remote.updated_at {
760 let local_json = serde_json::to_value(local).unwrap_or_default();
762 let remote_json = serde_json::to_value(remote).unwrap_or_default();
763
764 if local_json != remote_json {
765 conflicts.push(SyncConflict {
766 entity_id: local.id.clone(),
767 entity_type: "workspace".to_string(),
768 local_version: local_json,
769 remote_version: remote_json,
770 resolution: ConflictResolution::Manual,
771 });
772 }
773 }
774
775 conflicts
776 }
777
778 async fn sync_with_local(
780 &self,
781 workspace: &mut Workspace,
782 directory_path: &str,
783 _watch_changes: bool,
784 ) -> Result<SyncResult, String> {
785 let dir_path = Path::new(directory_path);
786
787 if !dir_path.exists() {
789 fs::create_dir_all(dir_path)
790 .await
791 .map_err(|e| format!("Failed to create directory {}: {}", directory_path, e))?;
792 }
793
794 match self.config.sync_direction {
795 SyncDirection::LocalToRemote => {
796 let file_path = dir_path.join(format!("{}.yaml", workspace.id));
798 let content = serde_yaml::to_string(workspace)
799 .map_err(|e| format!("Failed to serialize workspace: {}", e))?;
800
801 fs::write(&file_path, content)
802 .await
803 .map_err(|e| format!("Failed to write workspace file: {}", e))?;
804
805 Ok(SyncResult {
806 success: true,
807 changes_count: 1,
808 conflicts: vec![],
809 error: None,
810 })
811 }
812 SyncDirection::RemoteToLocal => {
813 let file_path = dir_path.join(format!("{}.yaml", workspace.id));
815
816 if !file_path.exists() {
817 return Err(format!("Workspace file not found: {:?}", file_path));
818 }
819
820 let content = fs::read_to_string(&file_path)
821 .await
822 .map_err(|e| format!("Failed to read workspace file: {}", e))?;
823
824 let remote_workspace: Workspace = serde_yaml::from_str(&content)
825 .map_err(|e| format!("Failed to deserialize workspace: {}", e))?;
826
827 let conflicts = {
829 let mut conflicts = vec![];
830
831 if workspace.updated_at > remote_workspace.updated_at {
833 let local_json = serde_json::to_value(&*workspace).unwrap_or_default();
835 let remote_json =
836 serde_json::to_value(&remote_workspace).unwrap_or_default();
837 conflicts.push(SyncConflict {
838 entity_id: workspace.id.clone(),
839 entity_type: "workspace".to_string(),
840 local_version: local_json,
841 remote_version: remote_json,
842 resolution: ConflictResolution::Manual,
843 });
844 } else if workspace.updated_at == remote_workspace.updated_at {
845 let local_json = serde_json::to_value(&*workspace).unwrap_or_default();
847 let remote_json =
848 serde_json::to_value(&remote_workspace).unwrap_or_default();
849 if local_json != remote_json {
850 conflicts.push(SyncConflict {
852 entity_id: workspace.id.clone(),
853 entity_type: "workspace".to_string(),
854 local_version: local_json,
855 remote_version: remote_json,
856 resolution: ConflictResolution::Manual,
857 });
858 }
859 }
860
861 conflicts
862 };
863
864 if conflicts.is_empty() && remote_workspace.updated_at >= workspace.updated_at {
866 *workspace = remote_workspace;
867 Ok(SyncResult {
868 success: true,
869 changes_count: 1,
870 conflicts: vec![],
871 error: None,
872 })
873 } else {
874 Ok(SyncResult {
875 success: true,
876 changes_count: 0,
877 conflicts,
878 error: None,
879 })
880 }
881 }
882 SyncDirection::Bidirectional => {
883 let file_path = dir_path.join(format!("{}.yaml", workspace.id));
885
886 let mut conflicts = vec![];
887
888 if file_path.exists() {
889 let content = fs::read_to_string(&file_path)
890 .await
891 .map_err(|e| format!("Failed to read workspace file: {}", e))?;
892
893 let remote_workspace: Workspace = serde_yaml::from_str(&content)
894 .map_err(|e| format!("Failed to deserialize workspace: {}", e))?;
895
896 if remote_workspace.updated_at > workspace.updated_at {
898 let remote_version =
900 serde_json::to_value(&remote_workspace).unwrap_or_default();
901 conflicts.push(SyncConflict {
902 entity_id: workspace.id.clone(),
903 entity_type: "workspace".to_string(),
904 local_version: serde_json::to_value(&*workspace).unwrap_or_default(),
905 remote_version,
906 resolution: ConflictResolution::Manual,
907 });
908 }
909 }
910
911 let content = serde_yaml::to_string(workspace)
913 .map_err(|e| format!("Failed to serialize workspace: {}", e))?;
914
915 fs::write(&file_path, content)
916 .await
917 .map_err(|e| format!("Failed to write workspace file: {}", e))?;
918
919 Ok(SyncResult {
920 success: true,
921 changes_count: 1,
922 conflicts,
923 error: None,
924 })
925 }
926 }
927 }
928
929 pub fn resolve_conflicts(
931 &mut self,
932 resolutions: HashMap<EntityId, ConflictResolution>,
933 ) -> Result<usize, String> {
934 let mut resolved_count = 0;
935
936 for conflict in &self.conflicts.clone() {
937 if let Some(resolution) = resolutions.get(&conflict.entity_id) {
938 match resolution {
939 ConflictResolution::Local => {
940 resolved_count += 1;
942 }
943 ConflictResolution::Remote => {
944 resolved_count += 1;
946 }
947 ConflictResolution::Manual => {
948 continue;
950 }
951 }
952 }
953 }
954
955 self.resolved_conflicts += resolved_count;
957
958 self.conflicts.retain(|conflict| {
960 !resolutions.contains_key(&conflict.entity_id)
961 || matches!(resolutions.get(&conflict.entity_id), Some(ConflictResolution::Manual))
962 });
963
964 self.status.conflicts = self.conflicts.len();
965 if self.conflicts.is_empty() {
966 self.status.state = SyncState::Synced;
967 } else {
968 self.status.state = SyncState::HasConflicts;
969 }
970
971 Ok(resolved_count)
972 }
973
974 pub fn get_sync_stats(&self) -> SyncStats {
976 SyncStats {
977 total_syncs: self.total_syncs,
978 successful_syncs: self.successful_syncs,
979 failed_syncs: self.failed_syncs,
980 total_conflicts: self.conflicts.len(),
981 resolved_conflicts: self.resolved_conflicts,
982 last_sync_duration_ms: self.last_sync_duration_ms,
983 }
984 }
985
986 pub fn export_config(&self) -> Result<String, String> {
988 serde_json::to_string_pretty(&self.config)
989 .map_err(|e| format!("Failed to serialize sync config: {}", e))
990 }
991
992 pub fn import_config(&mut self, json_data: &str) -> Result<(), String> {
994 let config: SyncConfig = serde_json::from_str(json_data)
995 .map_err(|e| format!("Failed to deserialize sync config: {}", e))?;
996
997 self.config = config;
998 Ok(())
999 }
1000
1001 pub fn has_pending_changes(&self) -> bool {
1003 self.status.pending_changes > 0
1004 }
1005
1006 pub fn get_manual_conflicts(&self) -> Vec<&SyncConflict> {
1008 self.conflicts
1009 .iter()
1010 .filter(|_conflict| {
1011 true
1013 })
1014 .collect()
1015 }
1016}
1017
1018#[derive(Debug, Clone, Serialize, Deserialize)]
1020pub struct SyncStats {
1021 pub total_syncs: usize,
1023 pub successful_syncs: usize,
1025 pub failed_syncs: usize,
1027 pub total_conflicts: usize,
1029 pub resolved_conflicts: usize,
1031 pub last_sync_duration_ms: Option<u64>,
1033}
1034
1035impl Default for SyncConfig {
1036 fn default() -> Self {
1037 Self {
1038 enabled: false,
1039 provider: SyncProvider::Local {
1040 directory_path: "./workspaces".to_string(),
1041 watch_changes: true,
1042 },
1043 interval_seconds: 300,
1044 conflict_strategy: ConflictResolutionStrategy::LocalWins,
1045 auto_commit: true,
1046 auto_push: false,
1047 directory_structure: SyncDirectoryStructure::PerWorkspace,
1048 sync_direction: SyncDirection::Bidirectional,
1049 }
1050 }
1051}
1052
1053impl Default for WorkspaceSyncManager {
1054 fn default() -> Self {
1055 Self::new(SyncConfig::default())
1056 }
1057}