Skip to main content

dot/commands/
add.rs

1use std::fs;
2use std::os::unix::fs::symlink;
3use std::path::{Path, PathBuf};
4
5use crate::commands::Command;
6use crate::error::{Error, Result};
7use crate::manifest::Manifest;
8
9pub struct AddCommand {
10    file_path: PathBuf,
11}
12
13impl AddCommand {
14    pub fn new(file_path: PathBuf) -> Self {
15        Self { file_path }
16    }
17
18    /// Core logic separated for testing with custom manifest
19    pub fn add_to_manifest(manifest: &mut Manifest, file_path: &Path) -> Result<PathBuf> {
20        let file_name = file_path
21            .file_name()
22            .ok_or_else(|| Error::NotFound(file_path.to_path_buf()))?;
23        let local_path = Path::new(file_name);
24
25        if manifest.contains(local_path) {
26            return Err(Error::AlreadyTracked(local_path.to_path_buf()));
27        }
28
29        // Move file to current directory
30        fs::rename(file_path, local_path)?;
31
32        // Create symlink at original location
33        let canonical = local_path.canonicalize()?;
34        symlink(&canonical, file_path)?;
35
36        // Update manifest
37        manifest.insert(local_path.to_path_buf(), file_path)?;
38
39        Ok(local_path.to_path_buf())
40    }
41}
42
43impl Command for AddCommand {
44    fn execute(self) -> Result<()> {
45        let mut manifest = Manifest::load()?;
46        let local_path = Self::add_to_manifest(&mut manifest, &self.file_path)?;
47        manifest.save()?;
48
49        log::info!("{} -> {}", local_path.display(), self.file_path.display());
50        Ok(())
51    }
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57    use tempfile::TempDir;
58
59    /// Test helper that adds a file using absolute paths
60    fn add_file(source_file: &Path, dest_dir: &Path) -> Result<PathBuf> {
61        let file_name = source_file
62            .file_name()
63            .ok_or_else(|| Error::NotFound(source_file.to_path_buf()))?;
64        let local_path = dest_dir.join(file_name);
65
66        // Move file to dest directory
67        fs::rename(source_file, &local_path)?;
68
69        // Create symlink at original location
70        let canonical = local_path.canonicalize()?;
71        symlink(&canonical, source_file)?;
72
73        Ok(local_path)
74    }
75
76    #[test]
77    fn rejects_already_tracked_file() {
78        let mut manifest = Manifest::empty();
79        manifest
80            .insert("testfile".into(), Path::new("/some/path"))
81            .unwrap();
82
83        // add_to_manifest checks manifest.contains() with just the filename
84        // So if manifest already has "testfile", adding any file named "testfile" should fail
85        assert!(manifest.contains(Path::new("testfile")));
86    }
87
88    #[test]
89    fn moves_file_and_creates_symlink() {
90        let repo = TempDir::new().unwrap();
91        let source_dir = TempDir::new().unwrap();
92        let source_file = source_dir.path().join("myconfig");
93        fs::write(&source_file, "content").unwrap();
94
95        let local_path = add_file(&source_file, repo.path()).unwrap();
96
97        // File exists in repo
98        assert!(local_path.exists());
99        assert_eq!(local_path, repo.path().join("myconfig"));
100        // Original is now a symlink
101        assert!(
102            source_file
103                .symlink_metadata()
104                .unwrap()
105                .file_type()
106                .is_symlink()
107        );
108    }
109
110    #[test]
111    fn returns_error_for_invalid_path() {
112        let repo = TempDir::new().unwrap();
113        let result = add_file(Path::new("/"), repo.path());
114        assert!(matches!(result, Err(Error::NotFound(_))));
115    }
116}