use std::borrow::Cow;
use std::cell::RefCell;
use std::fs;
use std::path::Path;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
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)]
enum State {
InSync,
Modified,
Missing,
}
#[derive(Debug, Eq, PartialEq)]
struct Status {
file: String,
state: State,
is_symlinked: bool,
}
impl PartialOrd for Status {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Status {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.file.cmp(&other.file)
}
}
pub fn status(root: Option<&String>, verbose: 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_status())?;
let statuses = 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 status = Status {
file: p.to_string_lossy().to_string(),
state: if destination.is_file() {
match are_files_equal(&source, &destination) {
Ok(equal) => {
if equal {
State::InSync
} else {
State::Modified
}
}
Err(err) => {
eprintln!(
"{error}: Could not compare '{}': {err}.",
source.display(),
error = ui::Color::error("error"),
);
return;
}
}
} else {
State::Missing
},
is_symlinked: destination.is_symlink(),
};
match statuses.lock() {
Ok(mut statuses) => {
statuses.push(status);
drop(statuses);
}
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 statuses = Arc::try_unwrap(statuses)
.expect("processing is over, we're back to a single thread.")
.into_inner()
.unwrap();
statuses.sort();
print_file_statuses(&statuses);
print_hooks(&hooks.list());
print_summary_line(&statuses);
nb_hooks_ran += run_hooks(|| hooks.post_status())?;
ui::print_hooks_summary(nb_hooks_ran);
if nb_errors.into_inner() > 0 {
Err(1)
} else {
Ok(())
}
}
fn are_files_equal(a: &Path, b: &Path) -> Result<bool, std::io::Error> {
if fs::metadata(a)?.len() != fs::metadata(b)?.len() {
return Ok(false);
}
thread_local! {
static BUFFERS: RefCell<(Vec<u8>, Vec<u8>)> = RefCell::new(
(Vec::with_capacity(65_536), Vec::with_capacity(65_536))
);
}
BUFFERS.with_borrow_mut(|(a_buf, b_buf)| {
utils::read_to_bytes_buffer(a_buf, a)?;
utils::read_to_bytes_buffer(b_buf, b)?;
Ok(a_buf == b_buf)
})
}
fn print_file_statuses(statuses: &[Status]) {
let summary = statuses
.iter()
.map(|s| {
format!(
" {} {}{}",
match &s.state {
State::InSync => ui::Color::in_sync("S"),
State::Modified => ui::Color::modified("M"),
State::Missing => ui::Color::missing("!"),
},
s.file,
if s.is_symlinked {
ui::Color::symlink("@")
} else {
ui::Color::none("")
},
)
})
.collect::<Vec<String>>()
.join("\n");
println!("Files\n{summary}");
}
fn print_hooks(hooks: &[Cow<str>]) {
if hooks.is_empty() {
return;
}
let summary = hooks
.iter()
.map(|h| format!(" {h}"))
.collect::<Vec<String>>()
.join("\n");
println!("Hooks\n{summary}");
}
fn print_summary_line(statuses: &[Status]) {
let (mut nb_in_sync, mut nb_modified, mut nb_missing) = (0, 0, 0);
for s in statuses {
match &s.state {
State::InSync => nb_in_sync += 1,
State::Modified => nb_modified += 1,
State::Missing => nb_missing += 1,
}
}
println!("{nb_in_sync} in sync, {nb_modified} modified, {nb_missing} missing.");
}