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 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 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 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 let local_file = repo.path().join("myfile");
95 fs::write(&local_file, "content").unwrap();
96
97 symlink(local_file.canonicalize().unwrap(), &original_path).unwrap();
99
100 remove_tracked_file(&local_file, &original_path).unwrap();
102
103 assert!(original_path.exists());
105 assert!(
106 !original_path
107 .symlink_metadata()
108 .unwrap()
109 .file_type()
110 .is_symlink()
111 );
112 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 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}