use logoxide::resolve::{Frozen, Update};
use logoxide_app::{
import::{Outcome, UpdatePlan},
importer::Importing,
};
use crate::{
app,
cli::{Cli, Format},
cmd::package::{DiffReport, print_skipped},
print_json, warn,
};
pub(crate) fn import(cli: &Cli, source: &str, vendored: bool) -> logoxide::Result<()> {
let source = match logoxide_app::import::check_source(source) {
Ok(()) => source.to_string(),
Err(error) => {
let dir = std::env::current_dir().map_err(|e| logoxide::LogoxideError::io(".", e))?;
logoxide_app::import::bundle_source(source, &dir).ok_or(error)?
}
};
let library = cli.library();
let mut project = cli.project(&library)?;
let imported = app().import(&mut project, &source, vendored, &mut warn())?;
print_skipped(&imported.skipped);
outln!(
"imported {} {} ({}) into {}",
imported.id,
imported.version,
imported.name,
imported.dir.display()
);
crate::cmd::project::warn_of_same_works(&project, &library);
Ok(())
}
pub(crate) fn import_update(
cli: &Cli,
name: Option<&str>,
dry_run: bool,
yes: bool,
) -> logoxide::Result<()> {
let library = cli.library();
let mut project = cli.project(&library)?;
let json = cli.format == Format::Json;
let plans = app().plan_updates(&project, name, &mut warn())?;
let importing = app().importing(&project);
let mut changed = std::collections::BTreeSet::new();
let (mut reports, mut failed) = (vec![], vec![]);
for plan in plans {
let mut report = ImportReport {
dependency: plan.dependency.clone(),
import: plan.import.clone(),
status: "current",
from: plan.from.clone(),
to: None,
upstream_from: plan.upstream_from.clone(),
upstream_to: None,
diff: None,
removed: vec![],
error: None,
};
let dep = plan.dependency.clone();
let options = UpdateOptions { dry_run, yes, json };
match update_import(plan, &mut report, options, &importing) {
Ok(()) if report.status == "updated" => {
changed.insert(dep);
}
Ok(()) => {}
Err(error) => {
errln!("lx: {dep}: {error}");
report.status = "failed";
report.error = Some(error.to_string());
failed.push(dep);
}
}
reports.push(report);
}
if reports.is_empty() && !json {
outln!("this project has no imports");
}
let locked = match changed.is_empty() {
true => Ok(()),
false => {
let update = Update::Only(changed);
let locked = app().install_with(&mut project, &importing, update, Frozen::default());
importing.warnings().into_iter().for_each(&mut warn());
locked.map(|_| ())
}
};
if json {
print_json(&reports)?;
}
locked?;
match failed.len() {
0 => Ok(()),
n => Err(logoxide::LogoxideError::Import(format!(
"{n} import{} not updated: {}",
if n == 1 { "" } else { "s" },
failed.join(", ")
))),
}
}
#[derive(serde::Serialize)]
#[serde(rename_all = "camelCase")]
struct ImportReport {
dependency: String,
import: String,
status: &'static str,
from: String,
to: Option<String>,
upstream_from: Option<String>,
upstream_to: Option<String>,
diff: Option<DiffReport>,
removed: Vec<String>,
error: Option<String>,
}
#[derive(Clone, Copy)]
struct UpdateOptions {
dry_run: bool,
yes: bool,
json: bool,
}
fn update_import(
plan: UpdatePlan,
report: &mut ImportReport,
options: UpdateOptions,
importing: &Importing<'_>,
) -> logoxide::Result<()> {
let UpdateOptions { dry_run, yes, json } = options;
let UpdatePlan {
dependency: dep,
import: import_name,
dir,
in_library,
from,
outcome,
..
} = plan;
let say = |text: String| {
if !json {
outln!("{text}");
}
};
let change = match outcome {
Outcome::Current => {
say(format!("{dep} {from}: up to date ({import_name})"));
return Ok(());
}
Outcome::Failed(error) => return Err(error),
Outcome::Changed(change) => change,
};
let update = &change.update;
let publisher = match (&update.upstream_from, &update.upstream_to) {
(Some(old), Some(new)) if old != new => format!("; the publisher's {old} → {new}"),
(None, Some(new)) => format!("; the publisher's {new}"),
_ => String::new(),
};
say(format!(
"{dep} {} → {}: {} (a {:?} change{publisher})",
update.from,
update.to,
logoxide::release::describe(&update.diff),
update.diff.change()
));
let removed = &change.removed;
report.status = "changed";
report.to = Some(update.to.to_string());
report.diff = Some(DiffReport::of(&update.diff));
report.upstream_to = update.upstream_to.clone();
report.removed = removed.clone();
if dry_run {
if !removed.is_empty() && !in_library {
say(format!(" would remove {}", removed.join(", ")));
}
return Ok(());
}
if in_library {
importing.prepare(&import_name, change.build()?);
report.status = "updated";
return Ok(());
}
if !removed.is_empty() && !yes {
use std::io::IsTerminal;
let what = format!(
"{dep}'s update removes {} file{} from {}: {}",
removed.len(),
if removed.len() == 1 { "" } else { "s" },
dir.display(),
removed.join(", ")
);
if !std::io::stdin().is_terminal() {
return Err(logoxide::LogoxideError::Import(format!(
"{what}; pass --yes to allow that"
)));
}
eprint!("{what}. Remove them? [y/N] ");
let mut answer = String::new();
let _ = std::io::stdin().read_line(&mut answer);
if !matches!(answer.trim(), "y" | "Y" | "yes") {
say(format!("{dep}: left as it was"));
report.status = "declined";
return Ok(());
}
}
logoxide_app::import::apply_update(&change, &dir, true)?;
report.status = "updated";
Ok(())
}