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,85 +251,155 @@ pub fn run(config_path: Option<PathBuf>, dry_run: bool, prune: bool) -> anyhow::
}
}
println!("\nHow to resolve conflicts?");
println!(" [s] Use source (overwrite target)");
println!(" [t] Keep target (skip these files)");
println!(" [i] Interactive (ask for each)");
println!(" [a] Abort");
print!("\nChoice [s/t/i/a]: ");
io::stdout().flush()?;
if dry_run {
println!("\nRun without --dry-run to resolve conflicts.");
} else {
println!("\nHow to resolve conflicts?");
println!(" [s] Use source (overwrite target)");
println!(" [t] Keep target (skip these files)");
println!(" [i] Interactive (ask for each)");
println!(" [a] Abort");
print!("\nChoice [s/t/i/a]: ");
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
match input.trim().to_lowercase().as_str() {
"s" => {
for (idx, _) in conflicts {
deploy_set.insert(idx);
match input.trim().to_lowercase().as_str() {
"s" => {
for (idx, _) in conflicts {
deploy_set.insert(idx);
}
}
}
"t" => {
println!("Skipping conflicted files.");
}
"i" => {
for (idx, status) in conflicts {
let dotfile = &result.dotfiles[idx];
let status_str = match status {
SyncStatus::TargetChanged => "target changed",
SyncStatus::Conflict => "both changed",
_ => "conflict",
};
println!(
"\n[{}] {} -> {}",
status_str,
dotfile.source.display(),
dotfile.target.display()
);
println!(
" [s] Use source [t] Keep target [d] Show diff [m] Merge in editor"
);
print!(" Choice [s/t/d/m]: ");
io::stdout().flush()?;
"t" => {
println!("Skipping conflicted files.");
}
"i" => {
for (idx, status) in conflicts {
let dotfile = &result.dotfiles[idx];
let status_str = match status {
SyncStatus::TargetChanged => "target changed",
SyncStatus::Conflict => "both changed",
_ => "conflict",
};
println!(
"\n[{}] {} -> {}",
status_str,
dotfile.source.display(),
dotfile.target.display()
);
println!(
" [s] Use source [t] Keep target [d] Show diff [m] Merge in editor"
);
print!(" Choice [s/t/d/m]: ");
io::stdout().flush()?;
let mut choice = String::new();
io::stdin().read_line(&mut choice)?;
let mut choice = String::new();
io::stdin().read_line(&mut choice)?;
match choice.trim().to_lowercase().as_str() {
"s" => {
deploy_set.insert(idx);
}
"d" => {
let full_source = source_dir.join(&dotfile.source);
show_diff(&full_source, &dotfile.target);
print!(" Use source? [y/n]: ");
io::stdout().flush()?;
let mut confirm = String::new();
io::stdin().read_line(&mut confirm)?;
if confirm.trim().to_lowercase() == "y" {
match choice.trim().to_lowercase().as_str() {
"s" => {
deploy_set.insert(idx);
}
}
"m" => {
let full_source = source_dir.join(&dotfile.source);
if merge_in_editor(&full_source, &dotfile.target)? {
// Source was updated with merged content, deploy it
deploy_set.insert(idx);
} else {
println!(" Merge cancelled, keeping target.");
"d" => {
let full_source = source_dir.join(&dotfile.source);
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]: ");
}
io::stdout().flush()?;
let mut confirm = String::new();
io::stdin().read_line(&mut confirm)?;
if confirm.trim().to_lowercase() == "y" {
deploy_set.insert(idx);
}
}
"m" => {
let full_source = source_dir.join(&dotfile.source);
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
deploy_set.insert(idx);
} else {
println!(" Merge cancelled, keeping target.");
}
}
_ => {
println!(" Keeping target.");
}
}
_ => {
println!(" Keeping target.");
}
}
}
_ => {
println!("Aborted.");
return Ok(());
}
}
_ => {
println!("Aborted.");
return Ok(());
}
}
} // else (not dry_run)
}
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) {
use std::process::Command;
if source.is_file() && target.is_file() {
let output = Command::new("diff")
.arg("--color=always")
.arg("-u")
.arg(target)
.arg(source)
.output();
if !source.is_file() {
println!(" (source does not exist: {})", source.display());
return;
}
if !target.is_file() {
println!(" (target does not exist: {})", target.display());
return;
}
if let Ok(output) = output {
println!("{}", String::from_utf8_lossy(&output.stdout));
match Command::new("diff")
.arg("--color=always")
.arg("-u")
.arg(target)
.arg(source)
.output()
{
Ok(output) => {
let stdout = String::from_utf8_lossy(&output.stdout);
if stdout.trim().is_empty() {
println!(" (no content differences)");
} else {
println!("{}", stdout);
}
}
Err(e) => {
println!(" (failed to run diff: {})", e);
}
} else {
println!(" (diff not available for directories)");
}
}

View file

@ -233,6 +233,10 @@ impl StateStore {
(false, false) => SyncStatus::Synced,
(true, false) => SyncStatus::SourceChanged,
(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,
};