1use std::fs;
2use std::path::Path;
3
4use anyhow::Result;
5use colored::Colorize;
6use diffy::merge;
7
8pub fn handle_backup_files(
31 old_version_dir: &Path,
32 new_version_dir: &Path,
33 backup_files: &[String],
34 scope: crate::pkg::types::Scope
35) -> Result<()> {
36 for backup_file_rel in backup_files {
37 let old_expanded = crate::pkg::utils::expand_placeholders(
38 backup_file_rel,
39 old_version_dir,
40 scope
41 )?;
42 let new_expanded = crate::pkg::utils::expand_placeholders(
43 backup_file_rel,
44 new_version_dir,
45 scope
46 )?;
47
48 let old_path = std::path::PathBuf::from(old_expanded);
49 let new_path = std::path::PathBuf::from(new_expanded);
50
51 let mut old_orig_path = old_path.clone();
53 let ext = old_orig_path
54 .extension()
55 .and_then(|s| s.to_str())
56 .map_or_else(|| "zoiorig".to_string(), |s| format!("{s}.zoiorig"));
57 old_orig_path.set_extension(ext);
58
59 if old_path.exists() {
60 if old_orig_path.exists()
62 && new_path.exists()
63 && let (Ok(base), Ok(yours), Ok(theirs)) = (
64 fs::read_to_string(&old_orig_path),
65 fs::read_to_string(&old_path),
66 fs::read_to_string(&new_path)
67 )
68 {
69 if yours == base {
70 continue;
72 }
73
74 if theirs == base {
75 if let Err(e) = fs::copy(&old_path, &new_path) {
77 eprintln!(
78 "Warning: failed to restore user config: {e}"
79 );
80 }
81 continue;
82 }
83
84 println!(
86 "{} Merging changes for '{}'...",
87 "::".bold().blue(),
88 backup_file_rel.cyan()
89 );
90 match merge(&base, &yours, &theirs) {
91 Ok(merged) => {
92 println!(
93 " {} Automatically merged.",
94 "Success:".green()
95 );
96 if let Err(e) = fs::write(&new_path, merged) {
97 eprintln!(
98 "Warning: failed to write merged config: {e}"
99 );
100 }
101 }
102 Err(conflicted) => {
103 eprintln!(
104 " {} Conflict in {}. Standard markers inserted.",
105 "Warning:".yellow().bold(),
106 backup_file_rel.bold()
107 );
108 let zoinew_path = new_path.with_extension(format!(
110 "{}.zoinew",
111 new_path
112 .extension()
113 .and_then(|s| s.to_str())
114 .unwrap_or_default()
115 ));
116 let _ = fs::copy(&new_path, &zoinew_path);
117
118 if let Err(e) = fs::write(&new_path, conflicted) {
119 eprintln!(
120 "Warning: failed to write conflicted config: \
121 {e}"
122 );
123 }
124 }
125 }
126 continue;
127 }
128
129 if new_path.exists() {
131 let zoinew_path = new_path.with_extension(format!(
132 "{}.zoinew",
133 new_path
134 .extension()
135 .and_then(|s| s.to_str())
136 .unwrap_or_default()
137 ));
138 println!(
139 "Configuration file '{}' exists in new version. Saving as \
140 .zoinew",
141 new_path.display()
142 );
143 if let Err(e) = fs::rename(&new_path, &zoinew_path) {
144 eprintln!("Warning: failed to rename to .zoinew: {e}");
145 continue;
146 }
147 }
148 if let Some(p) = new_path.parent() {
149 fs::create_dir_all(p)?;
150 }
151 if let Err(e) = fs::rename(&old_path, &new_path) {
152 eprintln!("Warning: failed to restore backup file: {e}");
153 }
154 }
155 }
156 Ok(())
157}
158
159#[cfg(test)]
160mod tests {
161 use tempfile::tempdir;
162
163 use super::*;
164
165 #[test]
166 fn test_merge_case_a_unmodified() {
167 let dir = tempdir().expect("Failed to create temp dir");
168 let old_dir = dir.path().join("1.0.0");
169 let new_dir = dir.path().join("1.1.0");
170 fs::create_dir_all(&old_dir).expect("Failed to create old dir");
171 fs::create_dir_all(&new_dir).expect("Failed to create new dir");
172
173 let base = "line1\nline2\n";
174
175 fs::write(old_dir.join("config.txt.zoiorig"), base)
176 .expect("Failed to write base");
177 fs::write(old_dir.join("config.txt"), base)
178 .expect("Failed to write yours");
179 fs::write(new_dir.join("config.txt"), "line1\nline2\nline3\n")
180 .expect("Failed to write theirs");
181
182 handle_backup_files(
183 &old_dir,
184 &new_dir,
185 &["config.txt".to_string()],
186 crate::pkg::types::Scope::User
187 )
188 .expect("Failed to handle backup files");
189
190 assert_eq!(
192 fs::read_to_string(new_dir.join("config.txt"))
193 .expect("Failed to read result"),
194 "line1\nline2\nline3\n"
195 );
196 }
197
198 #[test]
199 fn test_merge_case_b_upstream_unchanged() {
200 let dir = tempdir().expect("Failed to create temp dir");
201 let old_dir = dir.path().join("1.0.0");
202 let new_dir = dir.path().join("1.1.0");
203 fs::create_dir_all(&old_dir).expect("Failed to create old dir");
204 fs::create_dir_all(&new_dir).expect("Failed to create new dir");
205
206 let base = "line1\nline2\n";
207 fs::write(old_dir.join("config.txt.zoiorig"), base)
208 .expect("Failed to write base");
209 fs::write(old_dir.join("config.txt"), "line1\nline2\nuser_mod\n")
210 .expect("Failed to write yours");
211 fs::write(new_dir.join("config.txt"), base)
212 .expect("Failed to write theirs");
213
214 handle_backup_files(
215 &old_dir,
216 &new_dir,
217 &["config.txt".to_string()],
218 crate::pkg::types::Scope::User
219 )
220 .expect("Failed to handle backup files");
221
222 assert_eq!(
224 fs::read_to_string(new_dir.join("config.txt"))
225 .expect("Failed to read result"),
226 "line1\nline2\nuser_mod\n"
227 );
228 }
229
230 #[test]
231 fn test_merge_case_c_clean_merge() {
232 let dir = tempdir().expect("Failed to create temp dir");
233 let old_dir = dir.path().join("1.0.0");
234 let new_dir = dir.path().join("1.1.0");
235 fs::create_dir_all(&old_dir).expect("Failed to create old dir");
236 fs::create_dir_all(&new_dir).expect("Failed to create new dir");
237
238 let base = "common\n";
239 fs::write(old_dir.join("config.txt.zoiorig"), base)
240 .expect("Failed to write base");
241 fs::write(old_dir.join("config.txt"), "user_pref\ncommon\n")
242 .expect("Failed to write yours");
243 fs::write(new_dir.join("config.txt"), "common\nupstream_add\n")
244 .expect("Failed to write theirs");
245
246 handle_backup_files(
247 &old_dir,
248 &new_dir,
249 &["config.txt".to_string()],
250 crate::pkg::types::Scope::User
251 )
252 .expect("Failed to handle backup files");
253
254 let result = fs::read_to_string(new_dir.join("config.txt"))
255 .expect("Failed to read result");
256 assert!(result.contains("user_pref"));
257 assert!(result.contains("upstream_add"));
258 assert!(result.contains("common"));
259 }
260
261 #[test]
262 fn test_merge_case_c_conflict() {
263 let dir = tempdir().expect("Failed to create temp dir");
264 let old_dir = dir.path().join("1.0.0");
265 let new_dir = dir.path().join("1.1.0");
266 fs::create_dir_all(&old_dir).expect("Failed to create old dir");
267 fs::create_dir_all(&new_dir).expect("Failed to create new dir");
268
269 let base = "line\n";
270 fs::write(old_dir.join("config.txt.zoiorig"), base)
271 .expect("Failed to write base");
272 fs::write(old_dir.join("config.txt"), "user\n")
273 .expect("Failed to write yours");
274 fs::write(new_dir.join("config.txt"), "upstream\n")
275 .expect("Failed to write theirs");
276
277 handle_backup_files(
278 &old_dir,
279 &new_dir,
280 &["config.txt".to_string()],
281 crate::pkg::types::Scope::User
282 )
283 .expect("Failed to handle backup files");
284
285 let result = fs::read_to_string(new_dir.join("config.txt"))
286 .expect("Failed to read result");
287 assert!(result.contains("<<<<<<<"));
288 assert!(result.contains("user"));
289 assert!(result.contains("upstream"));
290
291 assert!(new_dir.join("config.txt.zoinew").exists());
292 }
293}