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