use color_eyre::eyre::Result;
use colored::*;
use crate::actions::{self, num_identical, num_unresolved_conflicts, Action, Actions};
use crate::scan::Change;
enum Resolution {
Local,
Remote,
}
fn resolve_action(action: &Action, resolution: Resolution) -> Action {
match action {
Action::Conflict(lc, rc)
| Action::ResolvedLocal((lc, rc), _)
| Action::ResolvedRemote((lc, rc), _) => match resolution {
Resolution::Local => match (lc, rc) {
(Change::Added(ln), Change::Added(rn)) => Action::ResolvedLocal(
(lc.clone(), rc.clone()),
Change::Modified(ln.clone(), rn.clone()),
),
(Change::Removed(_), Change::Modified(_, rn)) => {
Action::ResolvedLocal((lc.clone(), rc.clone()), Change::Added(rn.clone()))
}
(Change::Modified(_lo, ln), Change::Modified(_ro, rn)) => Action::ResolvedLocal(
(lc.clone(), rc.clone()),
Change::Modified(ln.clone(), rn.clone()),
),
(Change::Modified(_, ln), Change::Removed(_)) => {
Action::ResolvedLocal((lc.clone(), rc.clone()), Change::Removed(ln.clone()))
}
_ => unreachable!(),
},
Resolution::Remote => match (lc, rc) {
(Change::Added(ln), Change::Added(rn)) => Action::ResolvedRemote(
(lc.clone(), rc.clone()),
Change::Modified(rn.clone(), ln.clone()),
),
(Change::Modified(_, ln), Change::Removed(_rn)) => {
Action::ResolvedRemote((lc.clone(), rc.clone()), Change::Added(ln.clone()))
}
(Change::Modified(_lo, ln), Change::Modified(_ro, rn)) => Action::ResolvedRemote(
(lc.clone(), rc.clone()),
Change::Modified(rn.clone(), ln.clone()),
),
(Change::Removed(_ln), Change::Modified(_, rn)) => {
Action::ResolvedRemote((lc.clone(), rc.clone()), Change::Removed(rn.clone()))
}
_ => unreachable!(),
},
},
_ => action.clone(),
}
}
pub fn show_actions(actions: &Actions, verbose: bool) {
let num_identical = num_identical(actions.iter());
for a in actions {
if verbose || !a.is_identical() {
println!("{}", a);
}
}
if !verbose && num_identical > 0 {
println!(
"Skipped {} identical changes (use --verbose to show all)",
num_identical
);
}
}
#[derive(Debug)]
pub enum AllResolution {
Proceed,
Abort,
Force,
}
pub fn resolve_sequential(actions: &mut Actions, _verbose: bool) -> Result<AllResolution> {
use console::{Key, Term};
let term = Term::stdout();
if num_unresolved_conflicts(actions.iter()) > 0 {
term.write_line("Resolve conflicts:")?;
for a in actions {
if let Action::Conflict(_, _) = &a {
term.write_line(format!("{}", a).as_str())?;
term.write_line(actions::details(a).as_str())?;
loop {
term.write_line("left/l = update local, right/r = update remote, c = keep conflict, n/a = abort")?;
match term.read_key()? {
Key::ArrowLeft | Key::Char('l') => {
*a = resolve_action(&a, Resolution::Local);
}
Key::ArrowRight | Key::Char('r') => {
*a = resolve_action(&a, Resolution::Remote);
}
Key::Char('c') => {
}
Key::Char('a') => {
term.clear_last_lines(1)?;
return Ok(AllResolution::Abort);
}
_ => {
term.clear_last_lines(1)?;
continue;
}
}
term.clear_last_lines(3)?;
term.write_line(format!("{}", a).as_str())?;
break;
}
}
}
}
use dialoguer::Confirm;
if !Confirm::new()
.with_prompt("Do you want to continue?")
.interact()?
{
Ok(AllResolution::Abort)
} else {
Ok(AllResolution::Proceed)
}
}
pub fn resolve_interactive(actions: &mut Actions, verbose: bool) -> Result<AllResolution> {
use console::{Key, Term};
use std::ops::Rem;
let term = Term::stderr();
let (height, _width) = term.size();
let mut page = 0;
assert!(!actions.is_empty());
let mut actions: Vec<&mut Action> = actions
.iter_mut()
.filter(|a| verbose || !a.is_identical())
.collect();
let capacity = height as usize - 3;
let pages = (actions.len() as f64 / capacity as f64).ceil() as usize;
let mut sel = 0;
let mut height = 0;
let mut num_conflicts = num_unresolved_conflicts(actions.iter().map(|a| &**a));
let resolution = loop {
term.write_line(
format!(
"{}, n/a = abort, f = force{} [{}]",
if num_conflicts == 0 {
"y/g = proceed".bright_green()
} else {
"Tab/S-Tab = next/previous conflict".bright_yellow()
},
if actions[sel].is_conflict() {
", left/l = update local, right/r = update remote, c = keep conflict"
} else {
""
},
num_conflicts
)
.as_str(),
)?;
term.write_line(actions::details(&actions[sel]).as_str())?;
height += 2;
for (idx, action) in actions
.iter()
.enumerate()
.skip(page * capacity)
.take(capacity)
{
term.write_line(
format!("{} {}", (if sel == idx { ">" } else { " " }).cyan(), action).as_str(),
)?;
height += 1;
}
term.hide_cursor()?;
term.flush()?;
match term.read_key()? {
Key::ArrowDown | Key::Char('j') => {
loop {
sel = (sel as u64 + 1).rem(actions.len() as u64) as usize;
if verbose || !actions[sel].is_identical() {
break;
}
}
}
Key::ArrowUp | Key::Char('k') => {
loop {
sel =
((sel as i64 - 1 + actions.len() as i64) % (actions.len() as i64)) as usize;
if verbose || !actions[sel].is_identical() {
break;
}
}
}
Key::Tab => {
if let Some(next) = next_conflict_index(&actions, sel, 1) {
sel = next;
}
}
Key::BackTab => {
if let Some(previous) = next_conflict_index(&actions, sel, -1) {
sel = previous;
}
}
Key::ArrowLeft | Key::Char('l') => {
if actions[sel].is_conflict() {
if actions[sel].is_unresolved_conflict() {
num_conflicts -= 1;
}
*actions[sel] = resolve_action(&actions[sel], Resolution::Local);
}
sel = (sel as u64 + 1).rem(actions.len() as u64) as usize;
}
Key::ArrowRight | Key::Char('r') => {
if actions[sel].is_conflict() {
if actions[sel].is_unresolved_conflict() {
num_conflicts -= 1;
}
*actions[sel] = resolve_action(&actions[sel], Resolution::Remote);
}
sel = (sel as u64 + 1).rem(actions.len() as u64) as usize;
}
Key::Char('c') => {
if actions[sel].is_conflict() {
if !actions[sel].is_unresolved_conflict() {
match &actions[sel] {
Action::ResolvedLocal((lc, rc), _)
| Action::ResolvedRemote((lc, rc), _) => {
*actions[sel] = Action::Conflict(lc.clone(), rc.clone());
}
_ => unreachable!(),
}
num_conflicts += 1;
}
}
sel = (sel as u64 + 1).rem(actions.len() as u64) as usize;
}
Key::PageUp => {
if page == 0 {
page = pages - 1;
} else {
page -= 1;
}
sel = page * capacity;
}
Key::PageDown => {
if page == pages - 1 {
page = 0;
} else {
page += 1;
}
sel = page * capacity;
}
Key::Char('y') | Key::Char('g') if num_conflicts == 0 => {
break AllResolution::Proceed;
}
Key::Escape | Key::Char('a') | Key::Char('n') => {
break AllResolution::Abort;
}
Key::Char('f') => {
break AllResolution::Force;
}
_ => {}
}
if sel < page * capacity || sel >= (page + 1) * capacity {
page = sel / capacity;
}
term.clear_last_lines(height)?;
height = 0;
};
term.clear_last_lines(height)?;
term.show_cursor()?;
term.flush()?;
Ok(resolution)
}
fn next_conflict_index(actions: &[&mut Action], sel: usize, step: isize) -> Option<usize> {
if actions.is_empty() {
return None;
}
let len = actions.len() as isize;
let mut idx = sel as isize;
for _ in 0..actions.len() {
idx = (idx + step).rem_euclid(len);
if actions[idx as usize].is_conflict() {
return Some(idx as usize);
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::scan::DirEntryWithMeta;
use std::path::PathBuf;
fn entry(path: &str, checksum: u32) -> DirEntryWithMeta {
DirEntryWithMeta::test_file(PathBuf::from(path), checksum)
}
#[test]
fn local_resolution_turns_added_added_conflict_into_local_update() {
let local = Change::Added(entry("file.txt", 1));
let remote = Change::Added(entry("file.txt", 2));
let resolved = resolve_action(
&Action::Conflict(local.clone(), remote.clone()),
Resolution::Local,
);
match resolved {
Action::ResolvedLocal((original_local, original_remote), Change::Modified(from, to)) => {
assert_eq!(original_local.path(), local.path());
assert_eq!(original_remote.path(), remote.path());
assert_eq!(from.checksum(), 1);
assert_eq!(to.checksum(), 2);
}
_ => panic!("expected local resolution"),
}
}
#[test]
fn remote_resolution_turns_modified_removed_conflict_into_remote_add() {
let old = entry("file.txt", 1);
let local_new = entry("file.txt", 2);
let remote_old = entry("file.txt", 1);
let local = Change::Modified(old, local_new.clone());
let remote = Change::Removed(remote_old);
let resolved = resolve_action(
&Action::Conflict(local.clone(), remote.clone()),
Resolution::Remote,
);
match resolved {
Action::ResolvedRemote((original_local, original_remote), Change::Added(added)) => {
assert_eq!(original_local.path(), local.path());
assert_eq!(original_remote.path(), remote.path());
assert_eq!(added.checksum(), local_new.checksum());
}
_ => panic!("expected remote resolution"),
}
}
#[test]
fn next_conflict_index_returns_none_without_conflicts() {
let mut first = Action::Remote(Change::Added(entry("first.txt", 1)));
let mut second = Action::Remote(Change::Added(entry("second.txt", 2)));
let actions = vec![&mut first, &mut second];
assert_eq!(next_conflict_index(&actions, 0, 1), None);
assert_eq!(next_conflict_index(&actions, 0, -1), None);
}
#[test]
fn next_conflict_index_wraps_between_conflicts() {
let mut first = Action::Remote(Change::Added(entry("first.txt", 1)));
let mut second = Action::Conflict(
Change::Added(entry("second.txt", 2)),
Change::Added(entry("second.txt", 3)),
);
let mut third = Action::Remote(Change::Added(entry("third.txt", 4)));
let actions = vec![&mut first, &mut second, &mut third];
assert_eq!(next_conflict_index(&actions, 0, 1), Some(1));
assert_eq!(next_conflict_index(&actions, 2, 1), Some(1));
assert_eq!(next_conflict_index(&actions, 0, -1), Some(1));
}
}