use std::collections::{BTreeMap, BTreeSet};
use std::path::PathBuf;
use std::sync::Arc;
use crate::cli::args::{BackendArg, ToolArg};
use crate::config::tracking::Tracker;
use crate::config::{Config, Settings};
use crate::file::display_path;
use crate::runtime_symlinks;
use crate::toolset::{
NeededVersions, ToolVersion, ToolsetBuilder, get_versions_needed_by_tracked_configs,
get_versions_needed_by_tracked_stubs,
};
use crate::ui::install_progress::removal_progress;
use crate::ui::multi_progress_report::MultiProgressReport;
use crate::ui::prompt::{self, Confirmation};
use crate::{backend::Backend, config, env, exit};
use console::style;
use eyre::Result;
use super::trust::Trust;
#[derive(Debug, usage_rs::Args)]
#[usage(
verbatim_doc_comment,
example(
"mise prune --dry-run",
help = "Preview unused versions without deleting them. Example output: `rm -rf ~/.local/share/mise/installs/node/20.0.0` and `rm -rf ~/.local/share/mise/installs/node/20.0.1`."
)
)]
pub(crate) struct Prune {
#[usage()]
pub installed_tool: Option<Vec<ToolArg>>,
#[usage(long, short = 'n')]
pub dry_run: bool,
#[usage(long)]
pub configs: bool,
#[usage(long, verbatim_doc_comment)]
pub dry_run_code: bool,
#[usage(long, verbatim_doc_comment)]
pub monorepo: bool,
#[usage(long)]
pub tools: bool,
}
impl Prune {
pub(super) fn is_dry_run(&self) -> bool {
self.dry_run || self.dry_run_code
}
pub(crate) async fn run(self) -> Result<()> {
if self.monorepo {
unimplemented!("mise prune --monorepo is not implemented yet");
}
let mut config = Config::get().await?;
if self.configs || !self.tools {
self.prune_configs()?;
}
if self.tools || !self.configs {
let backends = self
.installed_tool
.as_ref()
.map(|it| it.iter().map(|ta| ta.ba.as_ref()).collect());
let tools = backends.unwrap_or_default();
let (to_delete, needed) = prunable_tools_with_sources(&config, tools).await?;
let has_work = !to_delete.is_empty();
let explain = self.is_dry_run().then_some(&needed);
delete(
&config,
self.is_dry_run(),
to_delete,
explain,
UnavailableConfirmation::Error,
)
.await?;
if self.dry_run_code && has_work {
return Err(exit::request(1));
}
if self.is_dry_run() {
return Ok(());
}
config = Config::reset().await?;
let ts = config.get_toolset().await?;
config::rebuild_shims_and_runtime_symlinks(
&config,
ts,
&[],
crate::lockfile::LockfileUpdateMode::Normal,
)
.await?;
}
Ok(())
}
fn prune_configs(&self) -> Result<()> {
if self.is_dry_run() {
info!("pruned configuration links {}", style("[dryrun]").bold());
} else {
Tracker::clean()?;
Trust::clean()?;
info!("pruned configuration links");
}
Ok(())
}
}
pub(crate) async fn prunable_tools(
config: &Arc<Config>,
tools: Vec<&BackendArg>,
) -> Result<Vec<(Arc<dyn Backend>, ToolVersion)>> {
Ok(prunable_tools_with_sources(config, tools).await?.0)
}
async fn prunable_tools_with_sources(
config: &Arc<Config>,
tools: Vec<&BackendArg>,
) -> Result<(Vec<(Arc<dyn Backend>, ToolVersion)>, NeededVersions)> {
let ts = ToolsetBuilder::new().build(config).await?;
let mut to_delete = ts
.list_installed_versions(config)
.await?
.into_iter()
.filter(|(_, tv)| {
env::install_path_category(&tv.install_path()) == env::InstallPathCategory::Local
})
.map(|(p, tv)| ((tv.ba().short.to_string(), tv.tv_pathname()), (p, tv)))
.collect::<BTreeMap<(String, String), (Arc<dyn Backend>, ToolVersion)>>();
if !tools.is_empty() {
to_delete.retain(|_, (_, tv)| tools.contains(&tv.ba()));
}
let mut needed = get_versions_needed_by_tracked_configs(config, true, true).await?;
for (key, sources) in get_versions_needed_by_tracked_stubs(config).await? {
needed.entry(key).or_default().extend(sources);
}
for key in needed.keys() {
to_delete.remove(key);
}
Ok((to_delete.into_values().collect(), needed))
}
pub(super) async fn prune(
config: &Arc<Config>,
tools: Vec<&BackendArg>,
dry_run: bool,
) -> Result<()> {
let to_delete = prunable_tools(config, tools).await?;
delete(
config,
dry_run,
to_delete,
None,
UnavailableConfirmation::Decline,
)
.await
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum UnavailableConfirmation {
Error,
Decline,
}
async fn delete(
config: &Arc<Config>,
dry_run: bool,
to_delete: Vec<(Arc<dyn Backend>, ToolVersion)>,
explain: Option<&NeededVersions>,
unavailable: UnavailableConfirmation,
) -> Result<()> {
let mpr = MultiProgressReport::get();
if dry_run {
for (p, tv) in to_delete {
if let Some(needed) = explain {
explain_removal(&tv, needed);
}
let prefix = format!("{} {} ", tv.style(), style("[dryrun]").bold());
let pr = mpr.add(&prefix);
p.uninstall_version(config, &tv, pr.as_ref(), true).await?;
pr.finish();
}
return Ok(());
}
let mut confirmed = Vec::with_capacity(to_delete.len());
for (p, tv) in to_delete {
if let Some(needed) = explain {
explain_removal(&tv, needed);
}
if Settings::get().yes {
confirmed.push((p, tv));
continue;
}
match prompt::confirm_with_all(format!("remove {} ?", tv))? {
Confirmation::Yes => confirmed.push((p, tv)),
Confirmation::No => {}
Confirmation::Unavailable if unavailable == UnavailableConfirmation::Decline => {}
Confirmation::Unavailable => eyre::bail!(
"mise prune requires confirmation but there was nobody to ask; pass --yes to prune non-interactively"
),
}
}
let mut progress = removal_progress(
&mpr,
confirmed
.iter()
.map(|(_, tv)| (removal_key(tv), tv.style())),
);
for (p, tv) in confirmed {
let tool = progress
.as_ref()
.and_then(|progress| progress.start_tool(&removal_key(&tv)));
let pr = match &tool {
Some(tool) => tool.reporter(),
None => mpr.add(&tv.style()),
};
let result = p.uninstall_version(config, &tv, pr.as_ref(), false).await;
if let Some(tool) = &tool {
tool.complete(result.as_ref().err().map(|e| e.to_string()).as_deref());
}
result?;
if let Err(err) = crate::tool_purgatory::forget_path(&tv.install_path()) {
warn!("failed to clear tool purgatory entry: {err:#}");
}
runtime_symlinks::remove_missing_symlinks(p)?;
if tool.is_none() {
pr.finish();
}
}
if let Some(progress) = progress.as_mut() {
progress.finish(vec![]);
}
Ok(())
}
fn removal_key(tv: &ToolVersion) -> String {
format!("{}@{}", tv.ba().short, tv.version)
}
fn explain_removal(tv: &ToolVersion, needed: &NeededVersions) {
let short = &tv.ba().short;
let kept: BTreeMap<&String, &BTreeSet<PathBuf>> = needed
.iter()
.filter(|((s, _), _)| s == short)
.map(|((_, version), sources)| (version, sources))
.collect();
let style = tv.style();
if kept.is_empty() {
info!("{style} is prunable: no tracked config or tool stub requires {short}");
return;
}
let kept = kept
.into_iter()
.map(|(version, sources)| {
let sources = sources.iter().map(display_path).collect::<Vec<_>>();
format!("{version} by {}", sources.join(", "))
})
.collect::<Vec<_>>();
info!(
"{style} is prunable: {short} is required at {}",
kept.join("; ")
);
}