feat(diff): fix diff bug in apply

This commit is contained in:
Ray Sinurat 2026-02-07 01:16:16 -06:00
parent f23a9b2653
commit bee2ceff00
2 changed files with 166 additions and 78 deletions

View file

@ -251,6 +251,9 @@ pub fn run(config_path: Option<PathBuf>, dry_run: bool, prune: bool) -> anyhow::
} }
} }
if dry_run {
println!("\nRun without --dry-run to resolve conflicts.");
} else {
println!("\nHow to resolve conflicts?"); println!("\nHow to resolve conflicts?");
println!(" [s] Use source (overwrite target)"); println!(" [s] Use source (overwrite target)");
println!(" [t] Keep target (skip these files)"); println!(" [t] Keep target (skip these files)");
@ -300,9 +303,36 @@ pub fn run(config_path: Option<PathBuf>, dry_run: bool, prune: bool) -> anyhow::
} }
"d" => { "d" => {
let full_source = source_dir.join(&dotfile.source); let full_source = source_dir.join(&dotfile.source);
show_diff(&full_source, &dotfile.target); if full_source.is_dir() {
// Show diffs for individual conflicted files
let relevant: Vec<_> = file_conflicts
.iter()
.filter(|(file_path, _, _, _)| {
file_path.starts_with(&dotfile.target)
})
.collect();
if relevant.is_empty() {
println!(" (no file-level diffs to show)");
} else {
for (file_path, src, tgt, _) in &relevant {
let relative = file_path
.strip_prefix(&dotfile.target)
.unwrap_or(file_path);
println!("\n diff: {}", relative.display());
show_diff(src, tgt);
}
}
print!(
" Use source for all {} conflicted file{}? [y/n]: ",
relevant.len(),
if relevant.len() == 1 { "" } else { "s" }
);
} else {
show_diff(&full_source, &dotfile.target);
print!(" Use source? [y/n]: "); print!(" Use source? [y/n]: ");
}
io::stdout().flush()?; io::stdout().flush()?;
let mut confirm = String::new(); let mut confirm = String::new();
io::stdin().read_line(&mut confirm)?; io::stdin().read_line(&mut confirm)?;
@ -312,7 +342,46 @@ pub fn run(config_path: Option<PathBuf>, dry_run: bool, prune: bool) -> anyhow::
} }
"m" => { "m" => {
let full_source = source_dir.join(&dotfile.source); let full_source = source_dir.join(&dotfile.source);
if merge_in_editor(&full_source, &dotfile.target)? { if full_source.is_dir() {
// Merge individual conflicted files
let relevant: Vec<_> = file_conflicts
.iter()
.filter(|(file_path, _, _, _)| {
file_path.starts_with(&dotfile.target)
})
.collect();
if relevant.is_empty() {
println!(" (no file-level conflicts to merge)");
} else {
let mut all_merged = true;
for (file_path, src, tgt, _) in &relevant {
let relative = file_path
.strip_prefix(&dotfile.target)
.unwrap_or(file_path);
println!("\n Merging {}...", relative.display());
if !merge_in_editor(src, tgt)? {
println!(
" Merge cancelled for {}.",
relative.display()
);
all_merged = false;
}
}
if all_merged {
deploy_set.insert(idx);
} else {
print!(
" Some files not merged. Deploy anyway? [y/n]: "
);
io::stdout().flush()?;
let mut confirm = String::new();
io::stdin().read_line(&mut confirm)?;
if confirm.trim().to_lowercase() == "y" {
deploy_set.insert(idx);
}
}
}
} else if merge_in_editor(&full_source, &dotfile.target)? {
// Source was updated with merged content, deploy it // Source was updated with merged content, deploy it
deploy_set.insert(idx); deploy_set.insert(idx);
} else { } else {
@ -330,6 +399,7 @@ pub fn run(config_path: Option<PathBuf>, dry_run: bool, prune: bool) -> anyhow::
return Ok(()); return Ok(());
} }
} }
} // else (not dry_run)
} }
let dry_prefix = if dry_run { "[dry-run] " } else { "" }; let dry_prefix = if dry_run { "[dry-run] " } else { "" };
@ -583,19 +653,33 @@ fn prompt_uninstall(package: &str) -> anyhow::Result<bool> {
fn show_diff(source: &PathBuf, target: &PathBuf) { fn show_diff(source: &PathBuf, target: &PathBuf) {
use std::process::Command; use std::process::Command;
if source.is_file() && target.is_file() { if !source.is_file() {
let output = Command::new("diff") println!(" (source does not exist: {})", source.display());
return;
}
if !target.is_file() {
println!(" (target does not exist: {})", target.display());
return;
}
match Command::new("diff")
.arg("--color=always") .arg("--color=always")
.arg("-u") .arg("-u")
.arg(target) .arg(target)
.arg(source) .arg(source)
.output(); .output()
{
if let Ok(output) = output { Ok(output) => {
println!("{}", String::from_utf8_lossy(&output.stdout)); let stdout = String::from_utf8_lossy(&output.stdout);
} if stdout.trim().is_empty() {
println!(" (no content differences)");
} else { } else {
println!(" (diff not available for directories)"); println!("{}", stdout);
}
}
Err(e) => {
println!(" (failed to run diff: {})", e);
}
} }
} }

View file

@ -233,6 +233,10 @@ impl StateStore {
(false, false) => SyncStatus::Synced, (false, false) => SyncStatus::Synced,
(true, false) => SyncStatus::SourceChanged, (true, false) => SyncStatus::SourceChanged,
(false, true) => SyncStatus::TargetChanged, (false, true) => SyncStatus::TargetChanged,
(true, true) if current_source_hash == current_target_hash => {
// Both changed but converged to the same content — not a real conflict
SyncStatus::SourceChanged
}
(true, true) => SyncStatus::Conflict, (true, true) => SyncStatus::Conflict,
}; };