dotstate 0.3.4

A modern, secure, and user-friendly dotfile manager built with Rust
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
//! Sync service for file synchronization operations.
//!
//! This module provides a service layer for file sync operations,
//! abstracting the details of file copying, symlink creation, and
//! manifest management from the UI layer.

use crate::config::Config;
use crate::file_manager::{copy_dir_all, Dotfile, FileManager};
use crate::utils::{get_home_dir, sync_validation, ProfileManifest, SymlinkManager};
use anyhow::{Context, Result};
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use tracing::{debug, info, warn};

/// Result of a sync validation.
#[derive(Debug)]
pub struct SyncValidationResult {
    /// Whether the operation is safe to perform.
    pub is_safe: bool,
    /// Error message if not safe.
    pub error_message: Option<String>,
}

/// Result of adding a file to sync.
#[derive(Debug)]
pub enum AddFileResult {
    /// File was successfully synced.
    Success,
    /// File was already synced, no action taken.
    AlreadySynced,
    /// Validation failed with the given error message.
    ValidationFailed(String),
}

/// Result of removing a file from sync.
#[derive(Debug)]
pub enum RemoveFileResult {
    /// File was successfully removed from sync.
    Success,
    /// File was not synced, no action taken.
    NotSynced,
}

/// Service for file synchronization operations.
///
/// This service provides a clean interface for file sync operations without
/// direct dependencies on UI state.
pub struct SyncService;

impl SyncService {
    /// Add a file to sync.
    ///
    /// This is the unified function that handles both regular dotfiles and custom files.
    /// It performs the following operations:
    /// 1. Validates the operation is safe
    /// 2. Copies the file to the repository
    /// 3. Creates a symlink from home to repo
    /// 4. Updates the profile manifest
    ///
    /// # Arguments
    ///
    /// * `config` - Application configuration.
    /// * `full_path` - Full path to the source file.
    /// * `relative_path` - Path relative to home directory.
    /// * `backup_enabled` - Whether to enable backups.
    ///
    /// # Returns
    ///
    /// Result indicating success, already synced, or validation failure.
    pub fn add_file_to_sync(
        config: &Config,
        full_path: &Path,
        relative_path: &str,
        backup_enabled: bool,
    ) -> Result<AddFileResult> {
        let profile_name = &config.active_profile;
        let repo_path = &config.repo_path;

        // Get previously synced files
        let previously_synced = Self::get_synced_files(repo_path, profile_name)?;

        // Check if already synced
        if previously_synced.contains(relative_path) {
            debug!("File already synced: {}", relative_path);
            return Ok(AddFileResult::AlreadySynced);
        }

        // VALIDATE BEFORE ANY OPERATIONS - prevent data loss
        let validation = sync_validation::validate_before_sync(
            relative_path,
            full_path,
            &previously_synced,
            repo_path,
        );
        if !validation.is_safe {
            let error_msg = validation
                .error_message
                .unwrap_or_else(|| "Cannot add this file or directory".to_string());
            warn!("Validation failed for {}: {}", relative_path, error_msg);
            return Ok(AddFileResult::ValidationFailed(error_msg));
        }

        // Create file manager for symlink resolution
        let file_manager = FileManager::new()?;

        // Validate symlink can be created before deleting original file
        let home_dir = get_home_dir();
        let target_path = home_dir.join(relative_path);
        let profile_path = repo_path.join(profile_name);
        let repo_file_path = profile_path.join(relative_path);

        // Handle symlinks: resolve to original file for validation
        let original_source = if file_manager.is_symlink(full_path) {
            file_manager.resolve_symlink(full_path)?
        } else {
            full_path.to_path_buf()
        };

        let symlink_validation = sync_validation::validate_symlink_creation(
            &original_source,
            &repo_file_path,
            &target_path,
        )
        .context("Failed to validate symlink creation")?;
        if !symlink_validation.is_safe {
            let error_msg = symlink_validation
                .error_message
                .unwrap_or_else(|| "Cannot create symlink".to_string());
            warn!(
                "Symlink validation failed for {}: {}",
                relative_path, error_msg
            );
            return Ok(AddFileResult::ValidationFailed(error_msg));
        }

        info!(
            "Adding file to sync: {} (profile: {})",
            relative_path, profile_name
        );
        debug!("Source path: {:?}", full_path);
        debug!("Repo destination: {:?}", repo_file_path);
        debug!("Symlink target: {:?}", target_path);

        // Create parent directories
        if let Some(parent) = repo_file_path.parent() {
            if !parent.exists() {
                debug!("Creating repo directory: {:?}", parent);
            }
            std::fs::create_dir_all(parent).context("Failed to create repo directory")?;
        }

        // Handle symlinks: resolve to original file
        let source_path = if file_manager.is_symlink(full_path) {
            debug!("Resolving symlink: {:?}", full_path);
            let resolved = file_manager.resolve_symlink(full_path)?;
            debug!("Resolved symlink to: {:?}", resolved);
            resolved
        } else {
            full_path.to_path_buf()
        };

        // Copy to repo FIRST (before deleting original)
        // This ensures we have a backup before any destructive operations
        info!("Copying file to repository...");
        file_manager
            .copy_to_repo(&source_path, &repo_file_path)
            .context("Failed to copy file to repo")?;
        info!("Successfully copied file to repository");

        // Create symlink using SymlinkManager
        info!("Creating symlink...");
        let mut symlink_mgr = SymlinkManager::new_with_backup(repo_path.clone(), backup_enabled)?;
        symlink_mgr
            .add_symlink_to_profile(profile_name, relative_path)
            .context("Failed to create symlink")?;
        info!("Successfully created symlink");

        // Update manifest
        info!("Updating profile manifest...");
        Self::add_file_to_manifest(repo_path, profile_name, relative_path)?;

        info!("Successfully added file to sync: {}", relative_path);
        Ok(AddFileResult::Success)
    }

    /// Remove a file from sync.
    ///
    /// This performs the following operations:
    /// 1. Removes the symlink from home
    /// 2. Restores the file from the repository
    /// 3. Removes the file from the repository
    /// 4. Updates the profile manifest
    ///
    /// # Arguments
    ///
    /// * `config` - Application configuration.
    /// * `relative_path` - Path relative to home directory.
    ///
    /// # Returns
    ///
    /// Result indicating success or that file was not synced.
    pub fn remove_file_from_sync(config: &Config, relative_path: &str) -> Result<RemoveFileResult> {
        let profile_name = &config.active_profile;
        let repo_path = &config.repo_path;
        let home_dir = get_home_dir();

        // Get previously synced files
        let previously_synced = Self::get_synced_files(repo_path, profile_name)?;

        if !previously_synced.contains(relative_path) {
            debug!("File not synced, skipping removal: {}", relative_path);
            return Ok(RemoveFileResult::NotSynced);
        }

        info!(
            "Removing file from sync: {} (profile: {})",
            relative_path, profile_name
        );

        let target_path = home_dir.join(relative_path);
        let repo_file_path = repo_path.join(profile_name).join(relative_path);

        // Restore file from repo if symlink exists
        if let Ok(metadata) = target_path.symlink_metadata() {
            if metadata.is_symlink() {
                // Remove symlink
                std::fs::remove_file(&target_path).context("Failed to remove symlink")?;

                // Copy file from repo back to home
                if repo_file_path.exists() {
                    if repo_file_path.is_dir() {
                        copy_dir_all(&repo_file_path, &target_path)
                            .context("Failed to restore directory from repo")?;
                    } else {
                        std::fs::copy(&repo_file_path, &target_path)
                            .context("Failed to restore file from repo")?;
                    }
                }
            }
        }

        // Update symlink tracking - remove only the specific file
        let mut symlink_mgr = SymlinkManager::new(repo_path.clone())?;

        // Remove the specific symlink from tracking
        // Note: We already removed the actual symlink and restored the file above (lines 227-244)
        // This just updates the tracking data without touching other symlinks
        symlink_mgr.remove_symlink_from_tracking(profile_name, relative_path)?;

        // Remove from repo
        if repo_file_path.exists() {
            if repo_file_path.is_dir() {
                std::fs::remove_dir_all(&repo_file_path)
                    .context("Failed to remove directory from repo")?;
            } else {
                std::fs::remove_file(&repo_file_path).context("Failed to remove file from repo")?;
            }
        }

        // Update manifest - remove the file from the synced files list
        let remaining_files: Vec<String> = previously_synced
            .iter()
            .filter(|f| *f != relative_path)
            .cloned()
            .collect();

        let mut manifest = ProfileManifest::load_or_backfill(repo_path)?;
        manifest.update_synced_files(profile_name, remaining_files)?;
        manifest.save(repo_path)?;

        info!("Successfully removed file from sync: {}", relative_path);
        Ok(RemoveFileResult::Success)
    }

    /// Get the set of synced files for a profile.
    ///
    /// # Arguments
    ///
    /// * `repo_path` - Path to the repository.
    /// * `profile_name` - Name of the profile.
    ///
    /// # Returns
    ///
    /// Set of synced file paths.
    pub fn get_synced_files(repo_path: &Path, profile_name: &str) -> Result<HashSet<String>> {
        let manifest = ProfileManifest::load_or_backfill(repo_path)?;
        Ok(manifest
            .profiles
            .into_iter()
            .find(|p| p.name == profile_name)
            .map(|p| p.synced_files.into_iter().collect())
            .unwrap_or_default())
    }

    /// Add a file path to the manifest for a profile.
    fn add_file_to_manifest(
        repo_path: &Path,
        profile_name: &str,
        relative_path: &str,
    ) -> Result<()> {
        let mut manifest = ProfileManifest::load_or_backfill(repo_path)?;
        let current_files = manifest
            .profiles
            .iter()
            .find(|p| p.name == profile_name)
            .map(|p| p.synced_files.clone())
            .unwrap_or_default();

        if current_files.contains(&relative_path.to_string()) {
            debug!("File already in manifest, skipping update");
        } else {
            debug!(
                "Adding {} to manifest for profile {}",
                relative_path, profile_name
            );
            let mut new_files = current_files;
            new_files.push(relative_path.to_string());
            manifest.update_synced_files(profile_name, new_files)?;
            manifest.save(repo_path)?;
            info!("Updated manifest with new file: {}", relative_path);
        }
        Ok(())
    }

    /// Scan for dotfiles and return the list.
    ///
    /// # Arguments
    ///
    /// * `config` - Application configuration.
    ///
    /// # Returns
    ///
    /// List of dotfiles found, with sync status marked.
    pub fn scan_dotfiles(config: &Config) -> Result<Vec<Dotfile>> {
        use crate::dotfile_candidates::get_default_dotfile_paths;

        let file_manager = FileManager::new()?;
        let dotfile_names = get_default_dotfile_paths();
        let mut found = file_manager.scan_dotfiles(&dotfile_names);

        debug!(
            "Found {} dotfiles from scan. Paths: {:?}",
            found.len(),
            found
                .iter()
                .map(|d| d.relative_path.to_string_lossy().to_string())
                .collect::<Vec<_>>()
        );

        // Load the manifest to get synced files and common files
        let manifest = ProfileManifest::load_or_backfill(&config.repo_path)?;

        // Mark files that are already synced
        let synced_set: HashSet<String> =
            Self::get_synced_files(&config.repo_path, &config.active_profile)
                .unwrap_or_default()
                .iter()
                .map(|p| {
                    let p = p.replace('\\', "/");
                    p.strip_prefix("./").unwrap_or(&p).to_string()
                })
                .collect();

        // Also check if any found files are common files
        let common_files_raw = manifest.get_common_files();
        debug!("Common files from manifest (raw): {:?}", common_files_raw);

        let common_files_set: HashSet<String> = common_files_raw
            .iter()
            .map(|p| {
                let p = p.replace('\\', "/");
                p.strip_prefix("./").unwrap_or(&p).to_string()
            })
            .collect();

        debug!("Common files set (normalized): {:?}", common_files_set);

        for dotfile in &mut found {
            let rel_raw = dotfile.relative_path.to_string_lossy().replace('\\', "/");
            let rel = rel_raw.strip_prefix("./").unwrap_or(&rel_raw).to_string();

            if synced_set.contains(&rel) {
                dotfile.synced = true;
            }
            if common_files_set.contains(&rel) {
                debug!(
                    "Marking file as common: {} (normalized: {})",
                    dotfile.relative_path.display(),
                    rel
                );
                dotfile.is_common = true;
                dotfile.synced = true; // Common files are always synced
            }
        }

        // Also add custom files from config
        let home_dir = get_home_dir();
        for custom_path in &config.custom_files {
            let full_path = home_dir.join(custom_path);
            let relative_path = PathBuf::from(custom_path);

            // Skip if not a valid path or if it doesn't exist
            if !full_path.exists() && !full_path.is_symlink() {
                continue;
            }

            // Check if already in the list
            if found
                .iter()
                .any(|d| d.relative_path.to_string_lossy() == *custom_path)
            {
                continue;
            }

            let is_synced = synced_set.contains(custom_path);

            found.push(Dotfile {
                original_path: full_path,
                relative_path,
                synced: is_synced,
                description: None,
                is_common: false,
                is_custom: true,
            });
        }

        // IMPORTANT: Also add synced files from manifest that aren't in the list yet
        // This ensures that custom files synced on another machine still show up
        // even if they're not in the local config.custom_files
        for synced_path in &synced_set {
            // Skip if already in the list
            if found
                .iter()
                .any(|d| d.relative_path.to_string_lossy() == *synced_path)
            {
                continue;
            }

            let full_path = home_dir.join(synced_path);
            let relative_path = PathBuf::from(synced_path);

            // Add even if file doesn't exist locally (might have been deleted)
            // This allows user to see and manage it in the UI
            found.push(Dotfile {
                original_path: full_path,
                relative_path,
                synced: true, // It's in the manifest, so it's synced
                description: None,
                is_common: false,
                is_custom: false,
            });
        }

        // Add common files from manifest (or mark existing files as common)
        let common_files = manifest.get_common_files();
        for common_path in common_files {
            let c_path_raw = common_path.replace('\\', "/");
            let c_path = c_path_raw.strip_prefix("./").unwrap_or(&c_path_raw);

            // Check if already in the list
            let existing_idx = found.iter().position(|d| {
                let d_path_raw = d.relative_path.to_string_lossy().replace('\\', "/");
                let d_path = d_path_raw.strip_prefix("./").unwrap_or(&d_path_raw);
                d_path == c_path
            });

            if let Some(idx) = existing_idx {
                // File already in list - mark it as common
                debug!("File {} already in list, marking as common", common_path);
                found[idx].is_common = true;
                found[idx].synced = true;
            } else {
                // File not in list - add it
                let full_path = home_dir.join(common_path);
                let relative_path = PathBuf::from(common_path);

                found.push(Dotfile {
                    original_path: full_path,
                    relative_path,
                    synced: true, // Common files are always synced
                    description: None,
                    is_common: true,
                    is_custom: false,
                });
            }
        }

        // Sort by relative path for consistent display
        found.sort_by(|a, b| a.relative_path.cmp(&b.relative_path));

        Ok(found)
    }

    /// Check if a path is a custom file (not in default dotfile candidates).
    ///
    /// # Arguments
    ///
    /// * `relative_path` - Path relative to home directory.
    ///
    /// # Returns
    ///
    /// True if this is a custom file.
    #[must_use]
    pub fn is_custom_file(relative_path: &str) -> bool {
        use crate::dotfile_candidates::get_default_dotfile_paths;
        let default_paths = get_default_dotfile_paths();
        !default_paths.iter().any(|p| p == relative_path)
    }

    // ============================================================================
    // Common File Methods - For files shared across all profiles
    // ============================================================================

    /// Add a file to common (shared across all profiles).
    ///
    /// This performs the following operations:
    /// 1. Validates the operation is safe
    /// 2. Copies the file to the common folder in the repository
    /// 3. Creates a symlink from home to the common folder
    /// 4. Updates the manifest
    ///
    /// # Arguments
    ///
    /// * `config` - Application configuration.
    /// * `full_path` - Full path to the source file.
    /// * `relative_path` - Path relative to home directory.
    /// * `backup_enabled` - Whether to enable backups.
    ///
    /// # Returns
    ///
    /// Result indicating success, already synced, or validation failure.
    pub fn add_common_file_to_sync(
        config: &Config,
        full_path: &Path,
        relative_path: &str,
        backup_enabled: bool,
    ) -> Result<AddFileResult> {
        let repo_path = &config.repo_path;

        // Load manifest to check if already in common
        let manifest = ProfileManifest::load_or_backfill(repo_path)?;
        if manifest.is_common_file(relative_path) {
            debug!("File already in common: {}", relative_path);
            return Ok(AddFileResult::AlreadySynced);
        }

        // VALIDATE BEFORE ANY OPERATIONS
        let previously_synced = Self::get_synced_files(repo_path, &config.active_profile)?;
        let validation = sync_validation::validate_before_sync(
            relative_path,
            full_path,
            &previously_synced,
            repo_path,
        );
        if !validation.is_safe {
            let error_msg = validation
                .error_message
                .unwrap_or_else(|| "Cannot add this file to common".to_string());
            warn!(
                "Validation failed for common file {}: {}",
                relative_path, error_msg
            );
            return Ok(AddFileResult::ValidationFailed(error_msg));
        }

        // Create file manager for symlink resolution
        let file_manager = FileManager::new()?;

        // Validate symlink can be created
        let home_dir = get_home_dir();
        let target_path = home_dir.join(relative_path);
        let common_path = repo_path.join("common");
        let repo_file_path = common_path.join(relative_path);

        // Handle symlinks: resolve to original file for validation
        let original_source = if file_manager.is_symlink(full_path) {
            file_manager.resolve_symlink(full_path)?
        } else {
            full_path.to_path_buf()
        };

        let symlink_validation = sync_validation::validate_symlink_creation(
            &original_source,
            &repo_file_path,
            &target_path,
        )
        .context("Failed to validate symlink creation")?;
        if !symlink_validation.is_safe {
            let error_msg = symlink_validation
                .error_message
                .unwrap_or_else(|| "Cannot create symlink".to_string());
            warn!(
                "Symlink validation failed for common file {}: {}",
                relative_path, error_msg
            );
            return Ok(AddFileResult::ValidationFailed(error_msg));
        }

        info!("Adding common file to sync: {}", relative_path);

        // Ensure common directory exists
        std::fs::create_dir_all(&common_path).context("Failed to create common directory")?;

        // Create parent directories
        if let Some(parent) = repo_file_path.parent() {
            if !parent.exists() {
                std::fs::create_dir_all(parent).context("Failed to create directory")?;
            }
        }

        // Handle symlinks: resolve to original file
        let source_path = if file_manager.is_symlink(full_path) {
            file_manager.resolve_symlink(full_path)?
        } else {
            full_path.to_path_buf()
        };

        // Copy to common folder in repo
        info!("Copying file to common folder...");
        file_manager
            .copy_to_repo(&source_path, &repo_file_path)
            .context("Failed to copy file to common folder")?;

        // Create symlink using SymlinkManager
        info!("Creating symlink...");
        let mut symlink_mgr = SymlinkManager::new_with_backup(repo_path.clone(), backup_enabled)?;
        symlink_mgr
            .add_common_symlink(relative_path)
            .context("Failed to create symlink")?;

        // Update manifest
        info!("Updating manifest...");
        let mut manifest = ProfileManifest::load_or_backfill(repo_path)?;
        manifest.add_common_file(relative_path);
        manifest.save(repo_path)?;

        info!("Successfully added common file: {}", relative_path);
        Ok(AddFileResult::Success)
    }

    /// Remove a file from common.
    ///
    /// This performs the following operations:
    /// 1. Removes the symlink from home
    /// 2. Restores the file from the common folder
    /// 3. Removes the file from the common folder
    /// 4. Updates the manifest
    ///
    /// # Arguments
    ///
    /// * `config` - Application configuration.
    /// * `relative_path` - Path relative to home directory.
    ///
    /// # Returns
    ///
    /// Result indicating success or that file was not in common.
    pub fn remove_common_file_from_sync(
        config: &Config,
        relative_path: &str,
    ) -> Result<RemoveFileResult> {
        let repo_path = &config.repo_path;
        let home_dir = get_home_dir();

        // Load manifest to check if in common
        let manifest = ProfileManifest::load_or_backfill(repo_path)?;
        if !manifest.is_common_file(relative_path) {
            debug!("File not in common, skipping removal: {}", relative_path);
            return Ok(RemoveFileResult::NotSynced);
        }

        info!("Removing common file from sync: {}", relative_path);

        let target_path = home_dir.join(relative_path);
        let common_path = repo_path.join("common");
        let repo_file_path = common_path.join(relative_path);

        // Restore file from common folder if symlink exists
        if let Ok(metadata) = target_path.symlink_metadata() {
            if metadata.is_symlink() {
                // Remove symlink
                std::fs::remove_file(&target_path).context("Failed to remove symlink")?;

                // Copy file from common folder back to home
                if repo_file_path.exists() {
                    if repo_file_path.is_dir() {
                        copy_dir_all(&repo_file_path, &target_path)
                            .context("Failed to restore directory from common")?;
                    } else {
                        std::fs::copy(&repo_file_path, &target_path)
                            .context("Failed to restore file from common")?;
                    }
                }
            }
        }

        // Update symlink tracking
        let mut symlink_mgr = SymlinkManager::new(repo_path.clone())?;
        symlink_mgr.remove_common_symlink_from_tracking(relative_path)?;

        // Remove from common folder
        if repo_file_path.exists() {
            if repo_file_path.is_dir() {
                std::fs::remove_dir_all(&repo_file_path)
                    .context("Failed to remove directory from common")?;
            } else {
                std::fs::remove_file(&repo_file_path)
                    .context("Failed to remove file from common")?;
            }
        }

        // Update manifest
        let mut manifest = ProfileManifest::load_or_backfill(repo_path)?;
        manifest.remove_common_file(relative_path);
        manifest.save(repo_path)?;

        info!("Successfully removed common file: {}", relative_path);
        Ok(RemoveFileResult::Success)
    }

    /// Move a file from a profile to common, cleaning up specified profiles.
    ///
    /// This is the validated version that handles cleanup of the same file
    /// in other profiles before moving to common.
    ///
    /// # Arguments
    ///
    /// * `config` - Application configuration.
    /// * `relative_path` - Path relative to home directory.
    /// * `profiles_to_cleanup` - Profiles that have the same file and should be cleaned up.
    ///
    /// # Returns
    ///
    /// Result indicating success or failure.
    pub fn move_to_common_with_cleanup(
        config: &Config,
        relative_path: &str,
        profiles_to_cleanup: &[String],
    ) -> Result<()> {
        let repo_path = &config.repo_path;
        let profile_name = &config.active_profile;

        info!(
            "Moving {} from profile '{}' to common (cleaning up {} profiles)",
            relative_path,
            profile_name,
            profiles_to_cleanup.len()
        );

        // Clean up the file from other profiles first
        for profile in profiles_to_cleanup {
            if profile == profile_name {
                continue; // Skip the source profile
            }

            info!("Cleaning up {} from profile '{}'", relative_path, profile);

            // Remove from manifest
            let mut manifest = ProfileManifest::load_or_backfill(repo_path)?;
            if let Some(p) = manifest.profiles.iter_mut().find(|p| p.name == *profile) {
                p.synced_files.retain(|f| f != relative_path);
            }
            manifest.save(repo_path)?;

            // Remove file from profile directory if it exists
            let profile_file_path = repo_path.join(profile).join(relative_path);
            if profile_file_path.exists() {
                if profile_file_path.is_dir() {
                    std::fs::remove_dir_all(&profile_file_path)
                        .context("Failed to remove directory from profile")?;
                } else {
                    std::fs::remove_file(&profile_file_path)
                        .context("Failed to remove file from profile")?;
                }
            }

            // Remove symlink tracking if it exists
            let mut symlink_mgr = SymlinkManager::new(repo_path.clone())?;
            symlink_mgr.remove_symlink_from_tracking(profile, relative_path)?;
        }

        // Now perform the normal move
        Self::move_to_common(config, relative_path)
    }

    /// Move a file from a profile to common.
    ///
    /// # Arguments
    ///
    /// * `config` - Application configuration.
    /// * `relative_path` - Path relative to home directory.
    ///
    /// # Returns
    ///
    /// Result indicating success or failure.
    pub fn move_to_common(config: &Config, relative_path: &str) -> Result<()> {
        let repo_path = &config.repo_path;
        let profile_name = &config.active_profile;

        info!(
            "Moving {} from profile '{}' to common",
            relative_path, profile_name
        );

        // Load manifest
        let mut manifest = ProfileManifest::load_or_backfill(repo_path)?;

        // Check if file is in the profile
        let profile = manifest
            .profiles
            .iter()
            .find(|p| p.name == *profile_name)
            .ok_or_else(|| anyhow::anyhow!("Profile '{profile_name}' not found"))?;

        if !profile.synced_files.contains(&relative_path.to_string()) {
            return Err(anyhow::anyhow!(
                "File '{relative_path}' is not synced in profile '{profile_name}'"
            ));
        }

        // Move the actual file from profile folder to common folder
        let profile_path = repo_path.join(profile_name);
        let common_path = repo_path.join("common");
        let source = profile_path.join(relative_path);
        let dest = common_path.join(relative_path);

        // Ensure common directory exists
        std::fs::create_dir_all(&common_path).context("Failed to create common directory")?;

        // Create parent directories for destination
        if let Some(parent) = dest.parent() {
            if !parent.exists() {
                std::fs::create_dir_all(parent)?;
            }
        }

        // Move the file
        if source.exists() {
            if source.is_dir() {
                copy_dir_all(&source, &dest)?;
                std::fs::remove_dir_all(&source)?;
            } else {
                std::fs::copy(&source, &dest)?;
                std::fs::remove_file(&source)?;
            }
        }

        // Update manifest first
        manifest.move_to_common(profile_name, relative_path)?;
        manifest.save(repo_path)?;

        // Update symlink to point to common folder using SymlinkManager
        // Disable backups since we're just updating a managed symlink (not replacing user's file)
        let mut symlink_mgr = SymlinkManager::new_with_backup(repo_path.clone(), false)?;
        symlink_mgr.remove_symlink_from_tracking(profile_name, relative_path)?;
        symlink_mgr.add_common_symlink(relative_path)?;

        info!("Successfully moved {} to common", relative_path);

        Ok(())
    }

    /// Move a file from common to the current profile.
    ///
    /// # Arguments
    ///
    /// * `config` - Application configuration.
    /// * `relative_path` - Path relative to home directory.
    ///
    /// # Returns
    ///
    /// Result indicating success or failure.
    pub fn move_from_common(config: &Config, relative_path: &str) -> Result<()> {
        let repo_path = &config.repo_path;
        let profile_name = &config.active_profile;

        info!(
            "Moving {} from common to profile '{}'",
            relative_path, profile_name
        );

        // Load manifest
        let mut manifest = ProfileManifest::load_or_backfill(repo_path)?;

        // Check if file is in common
        if !manifest.is_common_file(relative_path) {
            return Err(anyhow::anyhow!("File '{relative_path}' is not in common"));
        }

        // Move the actual file from common folder to profile folder
        let common_path = repo_path.join("common");
        let profile_path = repo_path.join(profile_name);
        let source = common_path.join(relative_path);
        let dest = profile_path.join(relative_path);

        // Create parent directories for destination
        if let Some(parent) = dest.parent() {
            if !parent.exists() {
                std::fs::create_dir_all(parent)?;
            }
        }

        // Move the file
        if source.exists() {
            if source.is_dir() {
                copy_dir_all(&source, &dest)?;
                std::fs::remove_dir_all(&source)?;
            } else {
                std::fs::copy(&source, &dest)?;
                std::fs::remove_file(&source)?;
            }
        }

        // Update manifest first
        manifest.move_from_common(profile_name, relative_path)?;
        manifest.save(repo_path)?;

        // Update symlink to point to profile folder using SymlinkManager
        // Disable backups since we're just updating a managed symlink (not replacing user's file)
        let mut symlink_mgr = SymlinkManager::new_with_backup(repo_path.clone(), false)?;
        symlink_mgr.remove_common_symlink_from_tracking(relative_path)?;
        symlink_mgr.add_symlink_to_profile(profile_name, relative_path)?;

        info!(
            "Successfully moved {} to profile '{}'",
            relative_path, profile_name
        );

        Ok(())
    }

    /// Get the set of common files.
    ///
    /// # Arguments
    ///
    /// * `repo_path` - Path to the repository.
    ///
    /// # Returns
    ///
    /// Set of common file paths.
    pub fn get_common_files(repo_path: &Path) -> Result<HashSet<String>> {
        let manifest = ProfileManifest::load_or_backfill(repo_path)?;
        Ok(manifest.get_common_files().iter().cloned().collect())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_is_custom_file() {
        // Common dotfiles should not be custom
        assert!(!SyncService::is_custom_file(".bashrc"));
        assert!(!SyncService::is_custom_file(".zshrc"));

        // Random files should be custom
        assert!(SyncService::is_custom_file("my_custom_config"));
        assert!(SyncService::is_custom_file(".my_app/config.toml"));
    }
    #[test]
    fn test_scan_dotfiles_path_normalization() {
        // Mock logic used in scan_dotfiles

        let local_path = "subdir/file";
        let local_path_win = "subdir\\file";
        let local_path_prefix = "./subdir/file";

        let manifest_path = "subdir/file";
        let manifest_path_win = "subdir\\file";
        let manifest_path_prefix = "./subdir/file";

        // Helper to simulate the cleanup logic
        let normalize = |p: &str| -> String {
            let p = p.replace('\\', "/");
            p.strip_prefix("./").unwrap_or(&p).to_string()
        };

        // Verify cross-platform matching
        assert_eq!(normalize(local_path), normalize(manifest_path));
        assert_eq!(normalize(local_path), normalize(manifest_path_win));
        assert_eq!(normalize(local_path), normalize(manifest_path_prefix));

        assert_eq!(normalize(local_path_win), normalize(manifest_path));
        assert_eq!(normalize(local_path_prefix), normalize(manifest_path));

        // Explicit cases
        assert_eq!(normalize("foo\\bar"), "foo/bar");
        assert_eq!(normalize("./foo/bar"), "foo/bar");
        assert_eq!(normalize(".\\foo\\bar"), "foo/bar");
    }
}