Skip to main content

dot/commands/
remove.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3
4use crate::commands::Command;
5use crate::error::{Error, Result};
6use crate::manifest::Manifest;
7
8pub struct RemoveCommand {
9    file_path: PathBuf,
10}
11
12impl RemoveCommand {
13    pub fn new(file_path: PathBuf) -> Self {
14        Self { file_path }
15    }
16
17    /// Core logic separated for testing
18    pub fn remove_from_manifest(manifest: &mut Manifest, file_path: &Path) -> Result<PathBuf> {
19        let symlink_path = manifest
20            .get(file_path)
21            .ok_or_else(|| Error::NotFound(file_path.to_path_buf()))?;
22
23        if !file_path.exists() {
24            return Err(Error::NotFound(file_path.to_path_buf()));
25        }
26
27        let metadata = symlink_path.symlink_metadata()?;
28        if !metadata.file_type().is_symlink() {
29            return Err(Error::NotASymlink(symlink_path.clone()));
30        }
31
32        // Remove symlink and restore file
33        fs::remove_file(&symlink_path)?;
34        fs::rename(file_path, &symlink_path)?;
35
36        manifest.remove(file_path);
37
38        Ok(symlink_path)
39    }
40}
41
42impl Command for RemoveCommand {
43    fn execute(self) -> Result<()> {
44        let mut manifest = Manifest::load()?;
45        let restored_path = Self::remove_from_manifest(&mut manifest, &self.file_path)?;
46        manifest.save()?;
47
48        println!(
49            "Removed {} (restored to {})",
50            self.file_path.display(),
51            restored_path.display()
52        );
53        Ok(())
54    }
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60    use std::os::unix::fs::symlink;
61    use tempfile::TempDir;
62
63    /// Test helper that removes a tracked file using absolute paths
64    fn remove_tracked_file(local_file: &Path, symlink_target: &Path) -> Result<()> {
65        let metadata = symlink_target.symlink_metadata()?;
66        if !metadata.file_type().is_symlink() {
67            return Err(Error::NotASymlink(symlink_target.to_path_buf()));
68        }
69
70        if !local_file.exists() {
71            return Err(Error::NotFound(local_file.to_path_buf()));
72        }
73
74        fs::remove_file(symlink_target)?;
75        fs::rename(local_file, symlink_target)?;
76
77        Ok(())
78    }
79
80    #[test]
81    fn returns_error_if_not_tracked() {
82        let mut manifest = Manifest::empty();
83        let result = RemoveCommand::remove_from_manifest(&mut manifest, Path::new("nottracked"));
84        assert!(matches!(result, Err(Error::NotFound(_))));
85    }
86
87    #[test]
88    fn restores_file_to_original_location() {
89        let repo = TempDir::new().unwrap();
90        let original_dir = TempDir::new().unwrap();
91        let original_path = original_dir.path().join("myfile");
92
93        // Create local file
94        let local_file = repo.path().join("myfile");
95        fs::write(&local_file, "content").unwrap();
96
97        // Create symlink at original location
98        symlink(local_file.canonicalize().unwrap(), &original_path).unwrap();
99
100        // Remove using test helper with absolute paths
101        remove_tracked_file(&local_file, &original_path).unwrap();
102
103        // Original is now a regular file
104        assert!(original_path.exists());
105        assert!(
106            !original_path
107                .symlink_metadata()
108                .unwrap()
109                .file_type()
110                .is_symlink()
111        );
112        // Local file is gone
113        assert!(!local_file.exists());
114    }
115
116    #[test]
117    fn returns_error_if_target_is_not_symlink() {
118        let repo = TempDir::new().unwrap();
119        let target_dir = TempDir::new().unwrap();
120        let target_path = target_dir.path().join("myfile");
121
122        // Create local file and regular file (not symlink) at target
123        let local_file = repo.path().join("myfile");
124        fs::write(&local_file, "local").unwrap();
125        fs::write(&target_path, "blocking").unwrap();
126
127        let result = remove_tracked_file(&local_file, &target_path);
128        assert!(matches!(result, Err(Error::NotASymlink(_))));
129    }
130}