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 { current: usize, total: usize },
198 Completed(SyncResult),
200 Failed(String),
202 ConflictDetected(SyncConflict),
204}
205
206impl WorkspaceSyncManager {
207 pub fn new(config: SyncConfig) -> Self {
209 let status = SyncStatus {
210 last_sync: None,
211 state: SyncState::NotSynced,
212 pending_changes: 0,
213 conflicts: 0,
214 last_error: None,
215 };
216
217 Self {
218 config,
219 status,
220 conflicts: Vec::new(),
221 total_syncs: 0,
222 successful_syncs: 0,
223 failed_syncs: 0,
224 resolved_conflicts: 0,
225 last_sync_duration_ms: None,
226 }
227 }
228
229 pub fn get_config(&self) -> &SyncConfig {
231 &self.config
232 }
233
234 pub fn update_config(&mut self, config: SyncConfig) {
236 self.config = config;
237 }
238
239 pub fn get_status(&self) -> &SyncStatus {
241 &self.status
242 }
243
244 pub fn get_conflicts(&self) -> &[SyncConflict] {
246 &self.conflicts
247 }
248
249 pub fn is_enabled(&self) -> bool {
251 self.config.enabled
252 }
253
254 pub async fn sync_workspace(
256 &mut self,
257 workspace: &mut Workspace,
258 ) -> Result<SyncResult, String> {
259 if !self.config.enabled {
260 return Err("Synchronization is disabled".to_string());
261 }
262
263 self.total_syncs += 1;
265
266 let start_time = std::time::Instant::now();
268
269 self.status.state = SyncState::Syncing;
270 self.status.last_error = None;
271
272 let result = match &self.config.provider {
273 SyncProvider::Git {
274 repo_url,
275 branch,
276 auth_token,
277 } => self.sync_with_git(workspace, repo_url, branch, auth_token.as_deref()).await,
278 SyncProvider::Cloud {
279 service_url,
280 api_key,
281 project_id,
282 } => self.sync_with_cloud(workspace, service_url, api_key, project_id).await,
283 SyncProvider::Local {
284 directory_path,
285 watch_changes,
286 } => self.sync_with_local(workspace, directory_path, *watch_changes).await,
287 };
288
289 let duration = start_time.elapsed();
291 let duration_ms = duration.as_millis() as u64;
292 self.last_sync_duration_ms = Some(duration_ms);
293
294 match &result {
295 Ok(sync_result) => {
296 if sync_result.success {
297 self.successful_syncs += 1;
298 self.status.state = SyncState::Synced;
299 self.status.last_sync = Some(Utc::now());
300 self.status.pending_changes = 0;
301 self.status.conflicts = sync_result.conflicts.len();
302 } else {
303 self.failed_syncs += 1;
304 self.status.state = SyncState::SyncFailed;
305 self.status.last_error = sync_result.error.clone();
306 }
307 }
308 Err(error) => {
309 self.failed_syncs += 1;
310 self.status.state = SyncState::SyncFailed;
311 self.status.last_error = Some(error.clone());
312 }
313 }
314
315 result
316 }
317
318 async fn sync_with_git(
320 &self,
321 workspace: &mut Workspace,
322 repo_url: &str,
323 branch: &str,
324 auth_token: Option<&str>,
325 ) -> Result<SyncResult, String> {
326 let temp_dir =
328 tempfile::tempdir().map_err(|e| format!("Failed to create temp directory: {}", e))?;
329
330 let repo_path = temp_dir.path().join("repo");
331
332 match self.config.sync_direction {
333 SyncDirection::LocalToRemote => {
334 self.sync_local_to_git(workspace, repo_url, branch, auth_token, &repo_path)
335 .await
336 }
337 SyncDirection::RemoteToLocal => {
338 self.sync_git_to_local(workspace, repo_url, branch, auth_token, &repo_path)
339 .await
340 }
341 SyncDirection::Bidirectional => {
342 self.sync_bidirectional_git(workspace, repo_url, branch, auth_token, &repo_path)
343 .await
344 }
345 }
346 }
347
348 async fn sync_local_to_git(
350 &self,
351 workspace: &Workspace,
352 repo_url: &str,
353 branch: &str,
354 auth_token: Option<&str>,
355 repo_path: &std::path::Path,
356 ) -> Result<SyncResult, String> {
357 self.ensure_git_repo(repo_url, branch, auth_token, repo_path).await?;
359
360 let workspace_file = repo_path.join(format!("{}.yaml", workspace.id));
362 let workspace_yaml = serde_yaml::to_string(workspace)
363 .map_err(|e| format!("Failed to serialize workspace: {}", e))?;
364
365 tokio::fs::write(&workspace_file, &workspace_yaml)
366 .await
367 .map_err(|e| format!("Failed to write workspace file: {}", e))?;
368
369 self.git_add_commit_push(repo_path, &workspace_file, auth_token).await?;
371
372 Ok(SyncResult {
373 success: true,
374 changes_count: 1,
375 conflicts: vec![],
376 error: None,
377 })
378 }
379
380 async fn sync_git_to_local(
382 &self,
383 workspace: &mut Workspace,
384 repo_url: &str,
385 branch: &str,
386 auth_token: Option<&str>,
387 repo_path: &std::path::Path,
388 ) -> Result<SyncResult, String> {
389 self.ensure_git_repo(repo_url, branch, auth_token, repo_path).await?;
391
392 let workspace_file = repo_path.join(format!("{}.yaml", workspace.id));
394
395 if !workspace_file.exists() {
396 return Ok(SyncResult {
397 success: true,
398 changes_count: 0,
399 conflicts: vec![],
400 error: None,
401 });
402 }
403
404 let workspace_yaml = tokio::fs::read_to_string(&workspace_file)
405 .await
406 .map_err(|e| format!("Failed to read workspace file: {}", e))?;
407
408 let remote_workspace: Workspace = serde_yaml::from_str(&workspace_yaml)
409 .map_err(|e| format!("Failed to deserialize workspace: {}", e))?;
410
411 let conflicts = self.detect_conflicts(workspace, &remote_workspace);
413
414 if conflicts.is_empty() {
415 *workspace = remote_workspace;
417 Ok(SyncResult {
418 success: true,
419 changes_count: 1,
420 conflicts: vec![],
421 error: None,
422 })
423 } else {
424 Ok(SyncResult {
426 success: true,
427 changes_count: 0,
428 conflicts,
429 error: None,
430 })
431 }
432 }
433
434 async fn sync_bidirectional_git(
436 &self,
437 workspace: &mut Workspace,
438 repo_url: &str,
439 branch: &str,
440 auth_token: Option<&str>,
441 repo_path: &std::path::Path,
442 ) -> Result<SyncResult, String> {
443 let pull_result = self
445 .sync_git_to_local(workspace, repo_url, branch, auth_token, repo_path)
446 .await?;
447
448 if !pull_result.conflicts.is_empty() {
449 return Ok(pull_result);
451 }
452
453 self.sync_local_to_git(workspace, repo_url, branch, auth_token, repo_path).await
455 }
456
457 async fn ensure_git_repo(
459 &self,
460 repo_url: &str,
461 branch: &str,
462 auth_token: Option<&str>,
463 repo_path: &std::path::Path,
464 ) -> Result<(), String> {
465 use std::process::Command;
466
467 let repo_path_str = repo_path.to_string_lossy();
469
470 if repo_path.exists() {
471 let output = Command::new("git")
473 .args(["-C", repo_path_str.as_ref(), "pull", "origin", branch])
474 .output()
475 .map_err(|e| format!("Failed to pull repository: {}", e))?;
476
477 if !output.status.success() {
478 let stderr = String::from_utf8_lossy(&output.stderr);
479 return Err(format!("Git pull failed: {}", stderr));
480 }
481 } else {
482 let clone_url = if let Some(token) = auth_token {
485 self.inject_auth_token_into_url(repo_url, token)
486 } else {
487 repo_url.to_string()
488 };
489
490 let output = Command::new("git")
491 .args([
492 "clone",
493 "--branch",
494 branch,
495 &clone_url,
496 repo_path_str.as_ref(),
497 ])
498 .output()
499 .map_err(|e| format!("Failed to clone repository: {}", e))?;
500
501 if !output.status.success() {
502 let stderr = String::from_utf8_lossy(&output.stderr);
503 return Err(format!("Git clone failed: {}", stderr));
504 }
505 }
506
507 Ok(())
508 }
509
510 async fn git_add_commit_push(
512 &self,
513 repo_path: &std::path::Path,
514 workspace_file: &std::path::Path,
515 _auth_token: Option<&str>,
516 ) -> Result<(), String> {
517 use std::process::Command;
518
519 let repo_path_str = repo_path.to_string_lossy();
521
522 let file_path_str = workspace_file
524 .strip_prefix(repo_path)
525 .unwrap_or(workspace_file)
526 .to_string_lossy();
527
528 let output = Command::new("git")
530 .args(["-C", repo_path_str.as_ref(), "add", file_path_str.as_ref()])
531 .output()
532 .map_err(|e| format!("Failed to add file to git: {}", e))?;
533
534 if !output.status.success() {
535 let stderr = String::from_utf8_lossy(&output.stderr);
536 return Err(format!("Git add failed: {}", stderr));
537 }
538
539 let status_output = Command::new("git")
541 .args(["-C", repo_path_str.as_ref(), "status", "--porcelain"])
542 .output()
543 .map_err(|e| format!("Failed to check git status: {}", e))?;
544
545 if status_output.stdout.is_empty() {
546 return Ok(());
548 }
549
550 let output = Command::new("git")
552 .args([
553 "-C",
554 repo_path_str.as_ref(),
555 "commit",
556 "-m",
557 "Update workspace",
558 ])
559 .output()
560 .map_err(|e| format!("Failed to commit changes: {}", e))?;
561
562 if !output.status.success() {
563 let stderr = String::from_utf8_lossy(&output.stderr);
564 return Err(format!("Git commit failed: {}", stderr));
565 }
566
567 let output = Command::new("git")
569 .args(["-C", repo_path_str.as_ref(), "push", "origin", "HEAD"])
570 .output()
571 .map_err(|e| format!("Failed to push changes: {}", e))?;
572
573 if !output.status.success() {
574 let stderr = String::from_utf8_lossy(&output.stderr);
575 return Err(format!("Git push failed: {}", stderr));
576 }
577
578 Ok(())
579 }
580
581 fn inject_auth_token_into_url(&self, url: &str, token: &str) -> String {
583 if let Some(https_pos) = url.find("https://") {
584 let rest = &url[https_pos + "https://".len()..];
585 format!("https://oauth2:{}@{}", token, rest)
586 } else {
587 url.to_string()
589 }
590 }
591
592 async fn sync_with_cloud(
594 &self,
595 workspace: &mut Workspace,
596 service_url: &str,
597 api_key: &str,
598 project_id: &str,
599 ) -> Result<SyncResult, String> {
600 let client = reqwest::Client::new();
602
603 let base_url = service_url.trim_end_matches('/');
605 let workspace_url =
606 format!("{}/api/v1/projects/{}/workspaces/{}", base_url, project_id, workspace.id);
607
608 match self.config.sync_direction {
609 SyncDirection::LocalToRemote => {
610 self.upload_workspace_to_cloud(&client, &workspace_url, api_key, workspace)
612 .await
613 }
614 SyncDirection::RemoteToLocal => {
615 self.download_workspace_from_cloud(&client, &workspace_url, api_key, workspace)
617 .await
618 }
619 SyncDirection::Bidirectional => {
620 self.bidirectional_sync(&client, &workspace_url, api_key, workspace).await
622 }
623 }
624 }
625
626 async fn upload_workspace_to_cloud(
628 &self,
629 client: &reqwest::Client,
630 workspace_url: &str,
631 api_key: &str,
632 workspace: &Workspace,
633 ) -> Result<SyncResult, String> {
634 let workspace_json = serde_json::to_string(workspace)
636 .map_err(|e| format!("Failed to serialize workspace: {}", e))?;
637
638 let response = client
640 .put(workspace_url)
641 .header("Authorization", format!("Bearer {}", api_key))
642 .header("Content-Type", "application/json")
643 .body(workspace_json)
644 .send()
645 .await
646 .map_err(|e| format!("Failed to upload workspace: {}", e))?;
647
648 if !response.status().is_success() {
649 let status = response.status();
650 let error_text = response.text().await.unwrap_or_default();
651 return Err(format!("Cloud upload failed with status {}: {}", status, error_text));
652 }
653
654 Ok(SyncResult {
655 success: true,
656 changes_count: 1,
657 conflicts: vec![],
658 error: None,
659 })
660 }
661
662 async fn download_workspace_from_cloud(
664 &self,
665 client: &reqwest::Client,
666 workspace_url: &str,
667 api_key: &str,
668 workspace: &mut Workspace,
669 ) -> Result<SyncResult, String> {
670 let response = client
672 .get(workspace_url)
673 .header("Authorization", format!("Bearer {}", api_key))
674 .send()
675 .await
676 .map_err(|e| format!("Failed to download workspace: {}", e))?;
677
678 if response.status() == reqwest::StatusCode::NOT_FOUND {
679 return Ok(SyncResult {
681 success: true,
682 changes_count: 0,
683 conflicts: vec![],
684 error: None,
685 });
686 }
687
688 if !response.status().is_success() {
689 let status = response.status();
690 let error_text = response.text().await.unwrap_or_default();
691 return Err(format!("Cloud download failed with status {}: {}", status, error_text));
692 }
693
694 let remote_json: serde_json::Value = response
695 .json()
696 .await
697 .map_err(|e| format!("Failed to parse remote workspace: {}", e))?;
698
699 let remote_workspace: Workspace = serde_json::from_value(remote_json.clone())
701 .map_err(|e| format!("Failed to deserialize remote workspace: {}", e))?;
702
703 let conflicts = self.detect_conflicts(workspace, &remote_workspace);
705
706 if conflicts.is_empty() {
708 *workspace = remote_workspace;
710 Ok(SyncResult {
711 success: true,
712 changes_count: 1,
713 conflicts: vec![],
714 error: None,
715 })
716 } else {
717 Ok(SyncResult {
719 success: true,
720 changes_count: 0,
721 conflicts,
722 error: None,
723 })
724 }
725 }
726
727 async fn bidirectional_sync(
729 &self,
730 client: &reqwest::Client,
731 workspace_url: &str,
732 api_key: &str,
733 workspace: &mut Workspace,
734 ) -> Result<SyncResult, String> {
735 let download_result = self
737 .download_workspace_from_cloud(client, workspace_url, api_key, workspace)
738 .await?;
739
740 if !download_result.conflicts.is_empty() {
741 return Ok(download_result);
743 }
744
745 self.upload_workspace_to_cloud(client, workspace_url, api_key, workspace).await
747 }
748
749 fn detect_conflicts(&self, local: &Workspace, remote: &Workspace) -> Vec<SyncConflict> {
751 let mut conflicts = vec![];
752
753 if local.updated_at > remote.updated_at {
755 let local_json = serde_json::to_value(local).unwrap_or_default();
757 let remote_json = serde_json::to_value(remote).unwrap_or_default();
758
759 if local_json != remote_json {
760 conflicts.push(SyncConflict {
761 entity_id: local.id.clone(),
762 entity_type: "workspace".to_string(),
763 local_version: local_json,
764 remote_version: remote_json,
765 resolution: ConflictResolution::Manual,
766 });
767 }
768 }
769
770 conflicts
771 }
772
773 async fn sync_with_local(
775 &self,
776 workspace: &mut Workspace,
777 directory_path: &str,
778 _watch_changes: bool,
779 ) -> Result<SyncResult, String> {
780 let dir_path = Path::new(directory_path);
781
782 if !dir_path.exists() {
784 fs::create_dir_all(dir_path)
785 .await
786 .map_err(|e| format!("Failed to create directory {}: {}", directory_path, e))?;
787 }
788
789 match self.config.sync_direction {
790 SyncDirection::LocalToRemote => {
791 let file_path = dir_path.join(format!("{}.yaml", workspace.id));
793 let content = serde_yaml::to_string(workspace)
794 .map_err(|e| format!("Failed to serialize workspace: {}", e))?;
795
796 fs::write(&file_path, content)
797 .await
798 .map_err(|e| format!("Failed to write workspace file: {}", e))?;
799
800 Ok(SyncResult {
801 success: true,
802 changes_count: 1,
803 conflicts: vec![],
804 error: None,
805 })
806 }
807 SyncDirection::RemoteToLocal => {
808 let file_path = dir_path.join(format!("{}.yaml", workspace.id));
810
811 if !file_path.exists() {
812 return Err(format!("Workspace file not found: {:?}", file_path));
813 }
814
815 let content = fs::read_to_string(&file_path)
816 .await
817 .map_err(|e| format!("Failed to read workspace file: {}", e))?;
818
819 let remote_workspace: Workspace = serde_yaml::from_str(&content)
820 .map_err(|e| format!("Failed to deserialize workspace: {}", e))?;
821
822 let conflicts = {
824 let mut conflicts = vec![];
825
826 if workspace.updated_at > remote_workspace.updated_at {
828 let local_json = serde_json::to_value(&*workspace).unwrap_or_default();
830 let remote_json =
831 serde_json::to_value(&remote_workspace).unwrap_or_default();
832 conflicts.push(SyncConflict {
833 entity_id: workspace.id.clone(),
834 entity_type: "workspace".to_string(),
835 local_version: local_json,
836 remote_version: remote_json,
837 resolution: ConflictResolution::Manual,
838 });
839 } else if workspace.updated_at == remote_workspace.updated_at {
840 let local_json = serde_json::to_value(&*workspace).unwrap_or_default();
842 let remote_json =
843 serde_json::to_value(&remote_workspace).unwrap_or_default();
844 if local_json != remote_json {
845 conflicts.push(SyncConflict {
847 entity_id: workspace.id.clone(),
848 entity_type: "workspace".to_string(),
849 local_version: local_json,
850 remote_version: remote_json,
851 resolution: ConflictResolution::Manual,
852 });
853 }
854 }
855
856 conflicts
857 };
858
859 if conflicts.is_empty() && remote_workspace.updated_at >= workspace.updated_at {
861 *workspace = remote_workspace;
862 Ok(SyncResult {
863 success: true,
864 changes_count: 1,
865 conflicts: vec![],
866 error: None,
867 })
868 } else {
869 Ok(SyncResult {
870 success: true,
871 changes_count: 0,
872 conflicts,
873 error: None,
874 })
875 }
876 }
877 SyncDirection::Bidirectional => {
878 let file_path = dir_path.join(format!("{}.yaml", workspace.id));
880
881 let mut conflicts = vec![];
882
883 if file_path.exists() {
884 let content = fs::read_to_string(&file_path)
885 .await
886 .map_err(|e| format!("Failed to read workspace file: {}", e))?;
887
888 let remote_workspace: Workspace = serde_yaml::from_str(&content)
889 .map_err(|e| format!("Failed to deserialize workspace: {}", e))?;
890
891 if remote_workspace.updated_at > workspace.updated_at {
893 let remote_version =
895 serde_json::to_value(&remote_workspace).unwrap_or_default();
896 conflicts.push(SyncConflict {
897 entity_id: workspace.id.clone(),
898 entity_type: "workspace".to_string(),
899 local_version: serde_json::to_value(&*workspace).unwrap_or_default(),
900 remote_version,
901 resolution: ConflictResolution::Manual,
902 });
903 }
904 }
905
906 let content = serde_yaml::to_string(workspace)
908 .map_err(|e| format!("Failed to serialize workspace: {}", e))?;
909
910 fs::write(&file_path, content)
911 .await
912 .map_err(|e| format!("Failed to write workspace file: {}", e))?;
913
914 Ok(SyncResult {
915 success: true,
916 changes_count: 1,
917 conflicts,
918 error: None,
919 })
920 }
921 }
922 }
923
924 pub fn resolve_conflicts(
926 &mut self,
927 resolutions: HashMap<EntityId, ConflictResolution>,
928 ) -> Result<usize, String> {
929 let mut resolved_count = 0;
930
931 for conflict in &self.conflicts.clone() {
932 if let Some(resolution) = resolutions.get(&conflict.entity_id) {
933 match resolution {
934 ConflictResolution::Local => {
935 resolved_count += 1;
937 }
938 ConflictResolution::Remote => {
939 resolved_count += 1;
941 }
942 ConflictResolution::Manual => {
943 continue;
945 }
946 }
947 }
948 }
949
950 self.resolved_conflicts += resolved_count;
952
953 self.conflicts.retain(|conflict| {
955 !resolutions.contains_key(&conflict.entity_id)
956 || matches!(resolutions.get(&conflict.entity_id), Some(ConflictResolution::Manual))
957 });
958
959 self.status.conflicts = self.conflicts.len();
960 if self.conflicts.is_empty() {
961 self.status.state = SyncState::Synced;
962 } else {
963 self.status.state = SyncState::HasConflicts;
964 }
965
966 Ok(resolved_count)
967 }
968
969 pub fn get_sync_stats(&self) -> SyncStats {
971 SyncStats {
972 total_syncs: self.total_syncs,
973 successful_syncs: self.successful_syncs,
974 failed_syncs: self.failed_syncs,
975 total_conflicts: self.conflicts.len(),
976 resolved_conflicts: self.resolved_conflicts,
977 last_sync_duration_ms: self.last_sync_duration_ms,
978 }
979 }
980
981 pub fn export_config(&self) -> Result<String, String> {
983 serde_json::to_string_pretty(&self.config)
984 .map_err(|e| format!("Failed to serialize sync config: {}", e))
985 }
986
987 pub fn import_config(&mut self, json_data: &str) -> Result<(), String> {
989 let config: SyncConfig = serde_json::from_str(json_data)
990 .map_err(|e| format!("Failed to deserialize sync config: {}", e))?;
991
992 self.config = config;
993 Ok(())
994 }
995
996 pub fn has_pending_changes(&self) -> bool {
998 self.status.pending_changes > 0
999 }
1000
1001 pub fn get_manual_conflicts(&self) -> Vec<&SyncConflict> {
1003 self.conflicts
1004 .iter()
1005 .filter(|_conflict| {
1006 true
1008 })
1009 .collect()
1010 }
1011}
1012
1013#[derive(Debug, Clone, Serialize, Deserialize)]
1015pub struct SyncStats {
1016 pub total_syncs: usize,
1018 pub successful_syncs: usize,
1020 pub failed_syncs: usize,
1022 pub total_conflicts: usize,
1024 pub resolved_conflicts: usize,
1026 pub last_sync_duration_ms: Option<u64>,
1028}
1029
1030impl Default for SyncConfig {
1031 fn default() -> Self {
1032 Self {
1033 enabled: false,
1034 provider: SyncProvider::Local {
1035 directory_path: "./workspaces".to_string(),
1036 watch_changes: true,
1037 },
1038 interval_seconds: 300,
1039 conflict_strategy: ConflictResolutionStrategy::LocalWins,
1040 auto_commit: true,
1041 auto_push: false,
1042 directory_structure: SyncDirectoryStructure::PerWorkspace,
1043 sync_direction: SyncDirection::Bidirectional,
1044 }
1045 }
1046}
1047
1048impl Default for WorkspaceSyncManager {
1049 fn default() -> Self {
1050 Self::new(SyncConfig::default())
1051 }
1052}