use std::borrow::Cow;
use std::cell::RefCell;
use std::path::Path;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use lessify::Pager;
use deezconfigs::{ui, utils, walk};
use super::common::{
determine_config_root, get_config_root_from_git, get_home_directory, get_hooks_for_command,
is_git_remote_uri, run_hooks,
};
#[derive(Debug, Eq, PartialEq)]
struct Diff {
file: String,
diff: String,
}
impl PartialOrd for Diff {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Diff {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.file.cmp(&other.file)
}
}
pub fn diff(root: Option<&String>, verbose: bool, reversed: bool) -> Result<(), i32> {
let root = if is_git_remote_uri(root) {
get_config_root_from_git(root.expect("not empty, contains a `git:` prefix"), verbose)?
} else {
determine_config_root(root, false)?
};
let home = get_home_directory()?;
let hooks = get_hooks_for_command(&root, &home, verbose)?;
let mut nb_hooks_ran = 0;
nb_hooks_ran += run_hooks(|| hooks.pre_diff())?;
let diffs = Arc::new(Mutex::new(Vec::with_capacity(20)));
let nb_errors = AtomicUsize::new(0);
walk::find_files_recursively(&root, |p| {
debug_assert!(!p.is_dir());
let source = root.join(p);
let destination = home.join(p);
let diff = if destination.is_file() {
let diff = if reversed {
diff_files(&destination, &source)
} else {
diff_files(&source, &destination)
};
let diff = match diff {
Ok(diff) => diff,
Err(err) => {
nb_errors.fetch_add(1, Ordering::Relaxed);
eprintln!(
"{error}: Could not compare '{}': {err}.",
p.display(),
error = ui::Color::error("error"),
);
return;
}
};
let Some(diff) = diff else {
return;
};
Diff {
file: p.to_string_lossy().to_string(),
diff,
}
} else {
Diff {
file: p.to_string_lossy().to_string(),
diff: String::from("! File does not exist in Home.\n! Skipping..."),
}
};
match diffs.lock() {
Ok(mut diffs) => {
diffs.push(diff);
drop(diffs);
}
Err(err) => {
nb_errors.fetch_add(1, Ordering::Relaxed);
eprintln!(
"{error}: Could not acquire lock: {err}.",
error = ui::Color::error("error"),
);
#[allow(clippy::needless_return)] return;
}
}
});
let mut diffs = Arc::try_unwrap(diffs)
.expect("processing is over, we're back to a single thread.")
.into_inner()
.unwrap();
diffs.sort();
nb_hooks_ran += run_hooks(|| hooks.post_diff())?;
let nb_errors = nb_errors.into_inner();
if nb_errors == 0 {
if diffs.is_empty() {
println!("Home is in sync.");
} else {
print_file_diffs(&diffs);
}
}
ui::print_hooks_summary(nb_hooks_ran);
if nb_errors > 0 { Err(1) } else { Ok(()) }
}
fn diff_files(before: &Path, after: &Path) -> Result<Option<String>, std::io::Error> {
use imara_diff::intern::InternedInput;
use imara_diff::{Algorithm, UnifiedDiffBuilder, diff};
thread_local! {
static BUFFERS: RefCell<(String, String)> = RefCell::new(
(String::with_capacity(65_536), String::with_capacity(65_536))
);
}
BUFFERS.with_borrow_mut(|(before_buf, after_buf)| {
utils::read_to_string_buffer(before_buf, before)?;
utils::read_to_string_buffer(after_buf, after)?;
let input = InternedInput::new(before_buf.as_str(), after_buf.as_str());
let diff = diff(
Algorithm::Histogram,
&input,
UnifiedDiffBuilder::new(&input),
);
if diff.is_empty() {
return Ok(None);
}
Ok(Some(diff))
})
}
fn print_file_diffs(diffs: &[Diff]) {
let diffs = diffs
.iter()
.map(|d| {
format!(
"{}\n{}\n",
ui::Color::file_name(&d.file),
d.diff
.lines()
.map(|l| {
match l.chars().next() {
Some('+') => ui::Color::in_sync(l),
Some('-' | '!') => ui::Color::missing(l),
Some('@') => ui::Color::line_range(l),
_ => Cow::Borrowed(l),
}
})
.collect::<Vec<Cow<str>>>()
.join("\n")
)
})
.collect::<Vec<String>>()
.join("\n");
Pager::page_or_print(&diffs);
}