1use camino::Utf8Path;
22use same_file::Handle;
23
24use crate::Result;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum AbsorbDecision {
28 InSync,
30 RelinkOnly,
33 AutoAbsorb,
37 NeedsConfirm,
41 Restore,
43}
44
45pub fn classify(source: &Utf8Path, target: &Utf8Path) -> Result<AbsorbDecision> {
46 let target_meta = match std::fs::symlink_metadata(target) {
48 Ok(m) => m,
49 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
50 return Ok(AbsorbDecision::Restore);
51 }
52 Err(e) => return Err(e.into()),
53 };
54
55 if let (Ok(src_h), Ok(dst_h)) = (
59 Handle::from_path(source.as_std_path()),
60 Handle::from_path(target.as_std_path()),
61 ) {
62 if src_h == dst_h {
63 return Ok(AbsorbDecision::InSync);
64 }
65 }
66
67 let source_meta = std::fs::metadata(source)?;
70 if target_meta.file_type().is_dir() && source_meta.file_type().is_dir() {
71 return Ok(AbsorbDecision::NeedsConfirm);
72 }
73
74 if target_meta.file_type().is_file() && source_meta.file_type().is_file() {
78 let identical = source_meta.len() == target_meta.len()
79 && std::fs::read(source)? == std::fs::read(target)?;
80 if identical {
81 return Ok(AbsorbDecision::RelinkOnly);
82 }
83 let src_mtime = source_meta.modified()?;
84 let dst_mtime = target_meta.modified()?;
85 if dst_mtime > src_mtime {
86 return Ok(AbsorbDecision::AutoAbsorb);
87 }
88 return Ok(AbsorbDecision::NeedsConfirm);
89 }
90
91 Ok(AbsorbDecision::NeedsConfirm)
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98 use camino::Utf8PathBuf;
99 use std::time::{Duration, SystemTime};
100 use tempfile::TempDir;
101
102 fn utf8(p: std::path::PathBuf) -> Utf8PathBuf {
103 Utf8PathBuf::from_path_buf(p).unwrap()
104 }
105
106 fn backdate(path: &Utf8Path, when: SystemTime) {
109 let f = std::fs::OpenOptions::new()
110 .write(true)
111 .open(path)
112 .expect("open writable for set_modified");
113 f.set_modified(when).expect("set_modified");
114 }
115
116 #[test]
117 fn missing_target_is_restore() {
118 let tmp = TempDir::new().unwrap();
119 let src = utf8(tmp.path().join("src.txt"));
120 std::fs::write(&src, "x").unwrap();
121 let dst = utf8(tmp.path().join("dst.txt"));
122 assert_eq!(classify(&src, &dst).unwrap(), AbsorbDecision::Restore);
123 }
124
125 #[test]
126 fn hardlink_is_in_sync() {
127 let tmp = TempDir::new().unwrap();
128 let src = utf8(tmp.path().join("src.txt"));
129 std::fs::write(&src, "x").unwrap();
130 let dst = utf8(tmp.path().join("dst.txt"));
131 std::fs::hard_link(&src, &dst).unwrap();
132 assert_eq!(classify(&src, &dst).unwrap(), AbsorbDecision::InSync);
133 }
134
135 #[test]
136 fn separate_files_same_content_is_relink_only() {
137 let tmp = TempDir::new().unwrap();
138 let src = utf8(tmp.path().join("src.txt"));
139 let dst = utf8(tmp.path().join("dst.txt"));
140 std::fs::write(&src, "same body").unwrap();
141 std::fs::write(&dst, "same body").unwrap();
142 assert_eq!(classify(&src, &dst).unwrap(), AbsorbDecision::RelinkOnly);
143 }
144
145 #[test]
146 fn target_newer_with_diff_is_auto_absorb() {
147 let tmp = TempDir::new().unwrap();
148 let src = utf8(tmp.path().join("src.txt"));
149 let dst = utf8(tmp.path().join("dst.txt"));
150 std::fs::write(&src, "old source").unwrap();
151 let past = SystemTime::now() - Duration::from_secs(60);
153 backdate(&src, past);
154 std::fs::write(&dst, "edited target").unwrap();
155 assert_eq!(classify(&src, &dst).unwrap(), AbsorbDecision::AutoAbsorb);
156 }
157
158 #[test]
159 fn source_newer_with_diff_is_needs_confirm() {
160 let tmp = TempDir::new().unwrap();
161 let src = utf8(tmp.path().join("src.txt"));
162 let dst = utf8(tmp.path().join("dst.txt"));
163 std::fs::write(&dst, "old target").unwrap();
164 let past = SystemTime::now() - Duration::from_secs(60);
165 backdate(&dst, past);
166 std::fs::write(&src, "fresh source").unwrap();
167 assert_eq!(classify(&src, &dst).unwrap(), AbsorbDecision::NeedsConfirm);
168 }
169
170 #[test]
171 fn separate_dirs_are_needs_confirm() {
172 let tmp = TempDir::new().unwrap();
173 let src = utf8(tmp.path().join("src_dir"));
174 let dst = utf8(tmp.path().join("dst_dir"));
175 std::fs::create_dir_all(&src).unwrap();
176 std::fs::create_dir_all(&dst).unwrap();
177 assert_eq!(classify(&src, &dst).unwrap(), AbsorbDecision::NeedsConfirm);
178 }
179}