use anyhow::Result;
use colored::*;
use diffy::merge;
use std::fs;
use std::path::Path;
pub fn handle_backup_files(
old_version_dir: &Path,
new_version_dir: &Path,
backup_files: &[String],
scope: crate::pkg::types::Scope,
) -> Result<()> {
for backup_file_rel in backup_files {
let old_expanded =
crate::pkg::utils::expand_placeholders(backup_file_rel, old_version_dir, scope)?;
let new_expanded =
crate::pkg::utils::expand_placeholders(backup_file_rel, new_version_dir, scope)?;
let old_path = std::path::PathBuf::from(old_expanded);
let new_path = std::path::PathBuf::from(new_expanded);
let mut old_orig_path = old_path.clone();
let ext = old_orig_path
.extension()
.and_then(|s| s.to_str())
.map(|s| format!("{}.zoiorig", s))
.unwrap_or_else(|| "zoiorig".to_string());
old_orig_path.set_extension(ext);
if old_path.exists() {
if old_orig_path.exists()
&& new_path.exists()
&& let (Ok(base), Ok(yours), Ok(theirs)) = (
fs::read_to_string(&old_orig_path),
fs::read_to_string(&old_path),
fs::read_to_string(&new_path),
)
{
if yours == base {
continue;
}
if theirs == base {
if let Err(e) = fs::copy(&old_path, &new_path) {
eprintln!("Warning: failed to restore user config: {}", e);
}
continue;
}
println!(
"{} Merging changes for '{}'...",
"::".bold().blue(),
backup_file_rel.cyan()
);
match merge(&base, &yours, &theirs) {
Ok(merged) => {
println!(" {} Automatically merged.", "Success:".green());
if let Err(e) = fs::write(&new_path, merged) {
eprintln!("Warning: failed to write merged config: {}", e);
}
}
Err(conflicted) => {
eprintln!(
" {} Conflict in {}. Standard markers inserted.",
"Warning:".yellow().bold(),
backup_file_rel.bold()
);
let zoinew_path = new_path.with_extension(format!(
"{}.zoinew",
new_path
.extension()
.and_then(|s| s.to_str())
.unwrap_or_default()
));
let _ = fs::copy(&new_path, &zoinew_path);
if let Err(e) = fs::write(&new_path, conflicted) {
eprintln!("Warning: failed to write conflicted config: {}", e);
}
}
}
continue;
}
if new_path.exists() {
let zoinew_path = new_path.with_extension(format!(
"{}.zoinew",
new_path
.extension()
.and_then(|s| s.to_str())
.unwrap_or_default()
));
println!(
"Configuration file '{}' exists in new version. Saving as .zoinew",
new_path.display()
);
if let Err(e) = fs::rename(&new_path, &zoinew_path) {
eprintln!("Warning: failed to rename to .zoinew: {}", e);
continue;
}
}
if let Some(p) = new_path.parent() {
fs::create_dir_all(p)?;
}
if let Err(e) = fs::rename(&old_path, &new_path) {
eprintln!("Warning: failed to restore backup file: {}", e);
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_merge_case_a_unmodified() {
let dir = tempdir().unwrap();
let old_dir = dir.path().join("1.0.0");
let new_dir = dir.path().join("1.1.0");
fs::create_dir_all(&old_dir).unwrap();
fs::create_dir_all(&new_dir).unwrap();
let _config_rel = "config.txt";
let base = "line1\nline2\n";
fs::write(old_dir.join("config.txt.zoiorig"), base).unwrap();
fs::write(old_dir.join("config.txt"), base).unwrap();
fs::write(new_dir.join("config.txt"), "line1\nline2\nline3\n").unwrap();
handle_backup_files(
&old_dir,
&new_dir,
&["config.txt".to_string()],
crate::pkg::types::Scope::User,
)
.unwrap();
assert_eq!(
fs::read_to_string(new_dir.join("config.txt")).unwrap(),
"line1\nline2\nline3\n"
);
}
#[test]
fn test_merge_case_b_upstream_unchanged() {
let dir = tempdir().unwrap();
let old_dir = dir.path().join("1.0.0");
let new_dir = dir.path().join("1.1.0");
fs::create_dir_all(&old_dir).unwrap();
fs::create_dir_all(&new_dir).unwrap();
let base = "line1\nline2\n";
fs::write(old_dir.join("config.txt.zoiorig"), base).unwrap();
fs::write(old_dir.join("config.txt"), "line1\nline2\nuser_mod\n").unwrap();
fs::write(new_dir.join("config.txt"), base).unwrap();
handle_backup_files(
&old_dir,
&new_dir,
&["config.txt".to_string()],
crate::pkg::types::Scope::User,
)
.unwrap();
assert_eq!(
fs::read_to_string(new_dir.join("config.txt")).unwrap(),
"line1\nline2\nuser_mod\n"
);
}
#[test]
fn test_merge_case_c_clean_merge() {
let dir = tempdir().unwrap();
let old_dir = dir.path().join("1.0.0");
let new_dir = dir.path().join("1.1.0");
fs::create_dir_all(&old_dir).unwrap();
fs::create_dir_all(&new_dir).unwrap();
let base = "common\n";
fs::write(old_dir.join("config.txt.zoiorig"), base).unwrap();
fs::write(old_dir.join("config.txt"), "user_pref\ncommon\n").unwrap();
fs::write(new_dir.join("config.txt"), "common\nupstream_add\n").unwrap();
handle_backup_files(
&old_dir,
&new_dir,
&["config.txt".to_string()],
crate::pkg::types::Scope::User,
)
.unwrap();
let result = fs::read_to_string(new_dir.join("config.txt")).unwrap();
assert!(result.contains("user_pref"));
assert!(result.contains("upstream_add"));
assert!(result.contains("common"));
}
#[test]
fn test_merge_case_c_conflict() {
let dir = tempdir().unwrap();
let old_dir = dir.path().join("1.0.0");
let new_dir = dir.path().join("1.1.0");
fs::create_dir_all(&old_dir).unwrap();
fs::create_dir_all(&new_dir).unwrap();
let base = "line\n";
fs::write(old_dir.join("config.txt.zoiorig"), base).unwrap();
fs::write(old_dir.join("config.txt"), "user\n").unwrap();
fs::write(new_dir.join("config.txt"), "upstream\n").unwrap();
handle_backup_files(
&old_dir,
&new_dir,
&["config.txt".to_string()],
crate::pkg::types::Scope::User,
)
.unwrap();
let result = fs::read_to_string(new_dir.join("config.txt")).unwrap();
assert!(result.contains("<<<<<<<"));
assert!(result.contains("user"));
assert!(result.contains("upstream"));
assert!(new_dir.join("config.txt.zoinew").exists());
}
}