use std::{
env,
fmt::{
self,
Write as _,
},
fs,
io::{
self,
IsTerminal as _,
},
path::{
Path,
PathBuf,
},
};
use clap::Parser as _;
#[cfg(feature = "json")] use dix::json;
use eyre::eyre;
use yansi::Paint as _;
struct WriteFmt<W: io::Write>(W);
impl<W: io::Write> fmt::Write for WriteFmt<W> {
fn write_str(&mut self, string: &str) -> fmt::Result {
self.0.write_all(string.as_bytes()).map_err(|_| fmt::Error)
}
}
#[derive(clap::Parser, Debug)]
#[command(version, about)]
struct Cli {
old_path: PathBuf,
new_path: PathBuf,
#[command(flatten)]
verbose: clap_verbosity_flag::Verbosity,
#[arg(
long,
default_value_t = clap::ColorChoice::Auto,
value_name = "WHEN",
global = true,
)]
color: clap::ColorChoice,
#[arg(long, default_value_t = false, global = true)]
force_correctness: bool,
#[arg(long, value_enum, default_value_t = OutputFormat::Human, global = true)]
output: OutputFormat,
}
#[derive(Debug, Clone, clap::ValueEnum, Eq, PartialEq)]
enum OutputFormat {
Human,
Json,
}
fn main() -> eyre::Result<()> {
let Cli {
old_path,
new_path,
verbose,
color,
force_correctness,
output,
} = Cli::parse();
tracing::debug!(
old_path = %old_path.display(),
new_path = %new_path.display(),
force_correctness = force_correctness,
"starting dix"
);
if !old_path.exists() {
tracing::error!(path = %old_path.display(), "old profile path does not exist");
return Err(eyre!(
"old profile path does not exist: {}",
old_path.display()
));
}
if !new_path.exists() {
tracing::error!(path = %new_path.display(), "new profile path does not exist");
return Err(eyre!(
"new profile path does not exist: {}",
new_path.display()
));
}
tracing::info!(old_path = %old_path.display(), new_path = %new_path.display(), "paths validated");
yansi::whenever(match color {
clap::ColorChoice::Auto => yansi::Condition::from(should_style),
clap::ColorChoice::Always => yansi::Condition::ALWAYS,
clap::ColorChoice::Never => yansi::Condition::NEVER,
});
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::builder()
.with_default_directive(match verbose.log_level_filter() {
clap_verbosity_flag::log::LevelFilter::Off
| clap_verbosity_flag::log::LevelFilter::Error => {
tracing::Level::ERROR.into()
},
clap_verbosity_flag::log::LevelFilter::Warn => {
tracing::Level::WARN.into()
},
clap_verbosity_flag::log::LevelFilter::Info => {
tracing::Level::INFO.into()
},
clap_verbosity_flag::log::LevelFilter::Debug => {
tracing::Level::DEBUG.into()
},
clap_verbosity_flag::log::LevelFilter::Trace => {
tracing::Level::TRACE.into()
},
})
.from_env_lossy(),
)
.with_ansi(should_style())
.with_target(false)
.without_time()
.init();
if force_correctness {
tracing::warn!(
"Falling back to slower but more robust backends (force_correctness is \
set)."
);
}
match output {
OutputFormat::Human => {
display_diff(&old_path, &new_path, force_correctness)?;
},
#[cfg(feature = "json")]
OutputFormat::Json => {
json::display_diff(&old_path, &new_path, force_correctness)?;
},
#[cfg(not(feature = "json"))]
OutputFormat::Json => {
return Err(eyre!(
"The 'json' feature is required to use '--json-output'."
));
},
}
Ok(())
}
fn display_diff(
old_path: &Path,
new_path: &Path,
force_correctness: bool,
) -> eyre::Result<()> {
let mut out = WriteFmt(io::stdout());
tracing::info!("starting diff computation");
writeln!(
out,
"{arrows} {old}",
arrows = "<<<".bold(),
old = fs::canonicalize(old_path)
.unwrap_or_else(|_| old_path.to_path_buf())
.display(),
)?;
writeln!(
out,
"{arrows} {new}",
arrows = ">>>".bold(),
new = fs::canonicalize(new_path)
.unwrap_or_else(|_| new_path.to_path_buf())
.display(),
)?;
let report = dix::query_diff_report(old_path, new_path, force_correctness)?;
dix::write_diff_report(&mut out, &report)?;
tracing::info!("diff computation complete");
Ok(())
}
fn should_style() -> bool {
if let Some(value) = env::var_os("NO_COLOR")
&& !value.is_empty()
{
return false;
}
if let Some(value) = env::var_os("CLICOLOR")
&& value == "0"
{
return false;
}
if let Some(value) = env::var_os("CLICOLOR_FORCE")
&& value != "0"
{
return true;
}
io::stdout().is_terminal()
}