feat(diff): fix diff bug in apply
This commit is contained in:
parent
f23a9b2653
commit
bee2ceff00
2 changed files with 166 additions and 78 deletions
|
|
@ -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)");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue