use anyhow::Result;
use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle};
use std::time::Duration;
use crate::{
application::cli::arguments::RollbackAction,
application::operations::rollback_operation::{
RollbackListRow, RollbackOperation, RollbackPackageOutcome, RollbackPackageStatus,
RollbackPreview, RollbackPreviewRow,
},
output::{self, Status, TransactionRow},
services::storage::rollback_storage::RollbackSource,
};
fn restore_phase_label(message: &str) -> &'static str {
if message.starts_with("Removing current installation for ") {
"Removing current install ..."
} else if message.starts_with("Restoring rollback artifact for ") {
"Restoring rollback artifact ..."
} else if message.starts_with("Restoring '") && message.contains("' to PATH") {
"Restoring PATH entries ..."
} else if message.starts_with("Restoring symlink for ") {
"Restoring runtime links ..."
} else if message.starts_with("Installing completion scripts for ") {
"Installing completions ..."
} else {
"Restoring rollback ..."
}
}
fn transaction_rows(rows: &[RollbackPreviewRow]) -> Vec<TransactionRow> {
rows.iter().map(TransactionRow::from).collect()
}
fn show_restore_preview(preview: &RollbackPreview) {
println!("{}", output::title("Rollback preview"));
if preview.rows.is_empty() {
println!(
"{}",
output::warning("No rollback artifacts found for selected packages.")
);
} else {
output::print_transaction_table(
&transaction_rows(&preview.rows),
&preview.impact,
"Net disk change:",
);
}
show_missing_names(&preview.missing_names);
}
fn show_prune_preview(preview: &RollbackPreview, dry_run: bool) {
if dry_run {
println!("{}", output::title("Rollback prune preview"));
}
if preview.rows.is_empty() {
println!("{}", output::warning("No rollback artifacts to prune."));
} else {
output::print_transaction_table(
&transaction_rows(&preview.rows),
&preview.impact,
"Net disk change:",
);
}
show_missing_names(&preview.missing_names);
}
fn show_missing_names(names: &[String]) {
for name in names {
output::status_line(Status::Fail, name, "no rollback data found");
}
}
pub fn run(action: RollbackAction) -> Result<()> {
let mut operation = RollbackOperation::new()?;
match action {
RollbackAction::Restore { names, dry_run } => run_restore(
resolve_restore_names(names, &operation)?,
dry_run,
&mut operation,
),
RollbackAction::Prune { names, dry_run } => run_prune(names, dry_run, &mut operation),
RollbackAction::List => run_list(&mut operation),
}
}
fn resolve_restore_names(names: Vec<String>, operation: &RollbackOperation) -> Result<Vec<String>> {
if !names.is_empty() {
return Ok(names);
}
let Some(names) = operation.latest_restore_names()? else {
println!(
"{}",
output::warning("No reversible transaction found in rollback history.")
);
return Ok(Vec::new());
};
Ok(names)
}
fn run_restore(names: Vec<String>, dry_run: bool, operation: &mut RollbackOperation) -> Result<()> {
if names.is_empty() {
return Ok(());
}
let preview = operation.restore_preview(&names)?;
if dry_run {
show_restore_preview(&preview.preview);
for target in &preview.targets {
output::status_line(
Status::Plan,
&target.name,
format!(
"restore rollback from {} ({:?})",
target.install_path, target.source
),
);
}
output::action_note("resolve only (no restore, no prune, no metadata changes)");
return Ok(());
}
show_restore_preview(&preview.preview);
let restorable_names = operation.restorable_names(&names);
if restorable_names.is_empty() {
println!(
"{}",
output::warning("No rollback artifacts to restore for selected packages.")
);
return Ok(());
}
output::confirm_or_cancel(
format!(
"Restore rollback for {} package(s)?",
restorable_names.len()
),
false,
)?;
let pb = ProgressBar::new_spinner();
pb.set_draw_target(ProgressDrawTarget::stderr_with_hz(10));
pb.set_style(ProgressStyle::with_template("{spinner:.green} {msg}")?);
pb.enable_steady_tick(Duration::from_millis(120));
pb.set_message("Restoring rollback");
let phase_pb = pb.clone();
let mut progress = Some(move |package_name: &str, line: &str| {
phase_pb.set_message(format!(
"Restoring rollback for {package_name}\n {:<28} {}",
package_name,
restore_phase_label(line)
));
});
let outcome = operation.restore(&restorable_names, &mut progress)?;
pb.finish_and_clear();
print_completion_lines(&outcome.packages);
if outcome.failed > 0 {
println!(
"{}",
output::warning(format!(
"Rollback complete: {} restored, {} failed.",
outcome.restored, outcome.failed
))
);
} else {
println!(
"{}",
output::success(format!(
"Rollback complete: {} restored, 0 failed.",
outcome.restored
))
);
}
Ok(())
}
fn run_list(operation: &mut RollbackOperation) -> Result<()> {
let rows = operation.list_rows();
if rows.is_empty() {
println!("{}", output::warning("No rollback artifacts found."));
return Ok(());
}
println!("{}", output::title("Rollback artifacts"));
print_list_rows(&rows);
Ok(())
}
fn run_prune(names: Vec<String>, dry_run: bool, operation: &mut RollbackOperation) -> Result<()> {
let preview = operation.prune_preview(names);
if dry_run {
show_prune_preview(&preview.preview, true);
output::action_note("resolve only (no prune, no metadata changes)");
return Ok(());
}
if !preview.target_names.is_empty() {
show_prune_preview(&preview.preview, false);
output::confirm_or_cancel(
format!(
"Prune rollback artifacts for {} package(s)?",
preview.target_names.len()
),
false,
)?;
}
let pb = ProgressBar::new_spinner();
pb.set_draw_target(ProgressDrawTarget::stderr_with_hz(10));
pb.set_style(ProgressStyle::with_template("{spinner:.green} {msg}")?);
pb.enable_steady_tick(Duration::from_millis(120));
pb.set_message("Pruning rollback artifacts");
let prune_pb = pb.clone();
let mut progress = Some(move |name: &str, current: usize, total: usize| {
prune_pb.set_message(format!(
"Pruning rollback artifacts for {:<28} ({}/{})",
name, current, total
));
});
let outcome = operation.prune(&preview.target_names, &mut progress);
pb.finish_and_clear();
let outcome = outcome?;
if preview.target_names.is_empty() {
println!("{}", output::warning("No rollback artifacts to prune."));
} else {
println!(
"{}",
output::success(format!(
"Rollback prune complete: {} pruned, {} missing.",
outcome.pruned, outcome.missing
))
);
}
Ok(())
}
fn print_completion_lines(packages: &[RollbackPackageOutcome]) {
let width = output::status_subject_width(packages.iter().map(|package| package.name.as_str()));
for package in packages {
match &package.status {
RollbackPackageStatus::Succeeded => {
println!(
"{}",
output::status_line_text_with_width(
Status::Ok,
&package.name,
"restored",
width
)
);
}
RollbackPackageStatus::Failed { error } => {
println!(
"{}",
output::status_line_text_with_width(Status::Fail, &package.name, error, width)
);
}
RollbackPackageStatus::Skipped { reason } => {
println!(
"{}",
output::status_line_text_with_width(Status::Warn, &package.name, reason, width)
);
}
}
}
}
fn print_list_rows(rows: &[RollbackListRow]) {
let name_width = rows
.iter()
.map(|row| row.name.chars().count())
.max()
.unwrap_or(4)
.max("Name".len())
.min(28);
let version_width = rows
.iter()
.map(|row| row.version.chars().count())
.max()
.unwrap_or(7)
.max("Version".len())
.min(18);
let source_width = "reinstall".len().max("Source".len());
let path_width = 72;
let table_width = name_width + version_width + source_width + path_width + 3;
println!(
"{:<name_width$} {:<version_width$} {:<source_width$} Install path",
"Name", "Version", "Source",
);
println!("{}", output::divider(table_width));
for row in rows {
println!(
"{:<name_width$} {:<version_width$} {:<source_width$} {}",
output::truncate_end(&row.name, name_width),
output::truncate_end(&row.version, version_width),
rollback_source_label(&row.source),
output::truncate_end(&row.install_path, path_width),
);
}
}
fn rollback_source_label(source: &RollbackSource) -> &'static str {
match source {
RollbackSource::Upgrade => "upgrade",
RollbackSource::Reinstall => "reinstall",
RollbackSource::Remove => "remove",
}
}