use clap::Args;
use serde::Serialize;
use crate::commands::Output;
use crate::error::Result;
use crate::fs::FileSystem;
use crate::vault::VaultManager;
#[derive(Args)]
pub struct DiffArgs {
pub path1: String,
pub path2: Option<String>,
#[arg(long = "v1")]
pub version1: Option<u64>,
#[arg(long = "v2")]
pub version2: Option<u64>,
#[arg(short, long)]
pub unified: bool,
}
#[derive(Serialize)]
struct DiffOutput {
path1: String,
path2: String,
version1: Option<u64>,
version2: Option<u64>,
identical: bool,
diff: Vec<DiffLine>,
}
#[derive(Serialize)]
struct DiffLine {
line_type: String, line_num: usize,
content: String,
}
pub fn run(args: DiffArgs, output: &Output, vault: Option<String>) -> Result<()> {
let manager = VaultManager::new()?;
let backend = match vault {
Some(name) => manager.open(&name)?,
None => manager.open_current()?,
};
let fs = FileSystem::new(backend.clone());
let (content1, label1) = if let Some(v1) = args.version1 {
let entry = fs.get_entry(&args.path1)?;
if !entry.is_file() {
return Err(crate::error::VfsError::NotAFile(
std::path::PathBuf::from(&args.path1),
));
}
let content = backend.get_version_content(entry.id, v1)?;
(content, format!("{} (v{})", args.path1, v1))
} else {
let content = fs.read_file(&args.path1)?;
(content, args.path1.clone())
};
let (content2, label2) = if let Some(v2) = args.version2 {
let entry = fs.get_entry(&args.path1)?;
let content = backend.get_version_content(entry.id, v2)?;
(content, format!("{} (v{})", args.path1, v2))
} else if let Some(ref path2) = args.path2 {
let content = fs.read_file(path2)?;
(content, path2.clone())
} else {
let entry = fs.get_entry(&args.path1)?;
let latest = backend.get_latest_version_number(entry.id)?;
if latest < 2 {
return Err(crate::error::VfsError::Internal(
"no previous version to compare".to_string(),
));
}
let content = backend.get_version_content(entry.id, latest - 1)?;
(content, format!("{} (v{})", args.path1, latest - 1))
};
let text1 = String::from_utf8_lossy(&content1);
let text2 = String::from_utf8_lossy(&content2);
let lines1: Vec<&str> = text1.lines().collect();
let lines2: Vec<&str> = text2.lines().collect();
let identical = content1 == content2;
if output.is_json() {
let diff_lines = compute_diff_lines(&lines1, &lines2);
output.print_json(&DiffOutput {
path1: label1,
path2: label2,
version1: args.version1,
version2: args.version2,
identical,
diff: diff_lines,
});
} else {
if identical {
println!("Files are identical");
} else {
println!("--- {}", label1);
println!("+++ {}", label2);
print_simple_diff(&lines1, &lines2);
}
}
Ok(())
}
fn compute_diff_lines(lines1: &[&str], lines2: &[&str]) -> Vec<DiffLine> {
let mut result = Vec::new();
let max_len = lines1.len().max(lines2.len());
for i in 0..max_len {
let l1 = lines1.get(i);
let l2 = lines2.get(i);
match (l1, l2) {
(Some(a), Some(b)) if a == b => {
result.push(DiffLine {
line_type: "context".to_string(),
line_num: i + 1,
content: a.to_string(),
});
}
(Some(a), Some(b)) => {
result.push(DiffLine {
line_type: "remove".to_string(),
line_num: i + 1,
content: a.to_string(),
});
result.push(DiffLine {
line_type: "add".to_string(),
line_num: i + 1,
content: b.to_string(),
});
}
(Some(a), None) => {
result.push(DiffLine {
line_type: "remove".to_string(),
line_num: i + 1,
content: a.to_string(),
});
}
(None, Some(b)) => {
result.push(DiffLine {
line_type: "add".to_string(),
line_num: i + 1,
content: b.to_string(),
});
}
(None, None) => {}
}
}
result
}
fn print_simple_diff(lines1: &[&str], lines2: &[&str]) {
let max_len = lines1.len().max(lines2.len());
for i in 0..max_len {
let l1 = lines1.get(i);
let l2 = lines2.get(i);
match (l1, l2) {
(Some(a), Some(b)) if a == b => {
println!(" {}", a);
}
(Some(a), Some(b)) => {
println!("-{}", a);
println!("+{}", b);
}
(Some(a), None) => {
println!("-{}", a);
}
(None, Some(b)) => {
println!("+{}", b);
}
(None, None) => {}
}
}
}