mod compute;
mod enumerator;
mod render;
mod types;
#[cfg(test)]
mod tests;
pub use types::{DiffMode, DiffOpts};
use rust_i18n::t;
use serde::Serialize;
use crate::core::{Metadata, VirtualenvService};
use crate::error::{Result, ScoopError};
use crate::output::Output;
use crate::uv::UvClient;
use compute::compute_package_diff;
use enumerator::{PackageEnumerator, UvPipEnumerator};
use types::{DiffData, DiffSummary, MetadataDiff, PackageDiff, ScalarDiff};
pub fn execute(output: &Output, opts: &DiffOpts) -> Result<()> {
let uv = UvClient::new()?;
let enumerator = UvPipEnumerator { uv: &uv };
let service = VirtualenvService::auto()?;
execute_with(output, opts, &enumerator, &service)
}
pub(crate) fn execute_with<E: PackageEnumerator>(
output: &Output,
opts: &DiffOpts,
enumerator: &E,
service: &VirtualenvService,
) -> Result<()> {
let (path_a, path_b) = resolve_env_paths(service, &opts.env_a, &opts.env_b)?;
let meta_a = service.read_metadata_result(&path_a)?;
let meta_b = service.read_metadata_result(&path_b)?;
let python = build_python_scalar(meta_a.as_ref(), meta_b.as_ref());
let packages = if opts.mode != DiffMode::MetadataOnly {
let a = enumerator.list(&path_a)?;
let b = enumerator.list(&path_b)?;
Some(compute_package_diff(&a, &b))
} else {
None
};
let metadata = if opts.mode != DiffMode::PackagesOnly {
Some(build_metadata_diff(meta_a.as_ref(), meta_b.as_ref()))
} else {
None
};
let summary = build_summary(&python, packages.as_ref(), metadata.as_ref());
let identical = summary.differences == 0;
let data = DiffData {
env_a: opts.env_a.clone(),
env_b: opts.env_b.clone(),
identical,
python,
packages,
metadata,
summary,
};
if output.is_json() {
emit_json_outcome(&data, opts.strict);
} else {
render::render_human(output, &data, opts.mode);
if opts.strict && !identical {
output.info(&t!("diff.hint_strict"));
}
}
if opts.strict && !identical {
return Err(ScoopError::DiffMismatch {
env_a: opts.env_a.clone(),
env_b: opts.env_b.clone(),
differences: data.summary.differences,
});
}
Ok(())
}
fn resolve_env_paths(
service: &VirtualenvService,
env_a: &str,
env_b: &str,
) -> Result<(std::path::PathBuf, std::path::PathBuf)> {
if !service.exists(env_a)? {
return Err(ScoopError::VirtualenvNotFound {
name: env_a.to_string(),
});
}
if !service.exists(env_b)? {
return Err(ScoopError::VirtualenvNotFound {
name: env_b.to_string(),
});
}
let path_a = crate::paths::virtualenv_path(env_a)?;
let path_b = crate::paths::virtualenv_path(env_b)?;
Ok((path_a, path_b))
}
fn build_python_scalar(a: Option<&Metadata>, b: Option<&Metadata>) -> ScalarDiff<String> {
ScalarDiff::from_sides(
a.map(|m| m.python_version.clone()),
b.map(|m| m.python_version.clone()),
)
}
fn build_metadata_diff(a: Option<&Metadata>, b: Option<&Metadata>) -> MetadataDiff {
MetadataDiff {
python_version: ScalarDiff::from_sides(
a.map(|m| m.python_version.clone()),
b.map(|m| m.python_version.clone()),
),
created_at: ScalarDiff::from_sides(
a.map(|m| m.created_at.to_rfc3339()),
b.map(|m| m.created_at.to_rfc3339()),
),
last_used: ScalarDiff::from_sides(
a.and_then(|m| m.last_used.map(|t| t.to_rfc3339())),
b.and_then(|m| m.last_used.map(|t| t.to_rfc3339())),
),
uv_version: ScalarDiff::from_sides(
a.and_then(|m| m.uv_version.clone()),
b.and_then(|m| m.uv_version.clone()),
),
}
}
fn build_summary(
python: &ScalarDiff<String>,
packages: Option<&PackageDiff>,
metadata: Option<&MetadataDiff>,
) -> DiffSummary {
let packages_added = packages.map(|p| p.added.len()).unwrap_or(0);
let packages_removed = packages.map(|p| p.removed.len()).unwrap_or(0);
let packages_changed = packages.map(|p| p.changed.len()).unwrap_or(0);
let metadata_fields_changed = metadata
.map(|m| {
[
m.created_at.changed,
m.last_used.changed,
m.uv_version.changed,
]
.iter()
.filter(|c| **c)
.count()
})
.unwrap_or(0);
let python_changed = python.changed;
let differences = (python_changed as usize)
+ packages_added
+ packages_removed
+ packages_changed
+ metadata_fields_changed;
DiffSummary {
differences,
python_changed,
packages_added,
packages_removed,
packages_changed,
metadata_fields_changed,
}
}
fn emit_json_outcome(data: &DiffData, strict: bool) {
#[derive(Serialize)]
struct SuccessEnvelope<'a> {
status: &'a str,
command: &'a str,
data: &'a DiffData,
}
#[derive(Serialize)]
struct FailureEnvelope<'a> {
status: &'a str,
command: &'a str,
error: ErrorBody<'a>,
data: &'a DiffData,
}
#[derive(Serialize)]
struct ErrorBody<'a> {
code: &'static str,
message: String,
env_a: &'a str,
env_b: &'a str,
differences: usize,
}
if strict && !data.identical {
let envelope = FailureEnvelope {
status: "error",
command: "diff",
error: ErrorBody {
code: "DIFF_MISMATCH",
message: t!(
"error.diff_mismatch",
a = data.env_a,
b = data.env_b,
n = data.summary.differences.to_string()
)
.to_string(),
env_a: &data.env_a,
env_b: &data.env_b,
differences: data.summary.differences,
},
data,
};
emit_envelope(&envelope);
} else {
let envelope = SuccessEnvelope {
status: "success",
command: "diff",
data,
};
emit_envelope(&envelope);
}
}
fn emit_envelope<T: Serialize>(envelope: &T) {
match serde_json::to_string(envelope) {
Ok(json) => println!("{json}"),
Err(err) => {
println!(
"{{\"status\":\"error\",\"command\":\"diff\",\"error\":{{\"code\":\"INTERNAL_JSON_ERROR\",\"message\":\"failed to serialise diff envelope: {}\"}}}}",
err.to_string().replace('"', "\\\"")
);
}
}
}