use super::{Choice, PromptResult};
use crate::agent::Agent;
use crate::package::Package;
use crate::package::manifest::DEFAULT_VERSION;
use crate::release::Release;
use crate::search_packages;
use crate::version::VersionExt;
use anyhow::Result;
use clap::Args;
use colored::Colorize;
use inquire::{Confirm, MultiSelect, Select};
use itertools::Itertools;
use serde::Deserialize;
use std::fmt;
use std::path::PathBuf;
use std::sync::OnceLock;
use strum::IntoEnumIterator;
use tokio::process::Command;
static RELEASE: OnceLock<Release> = OnceLock::new();
#[derive(Args, Debug, Default, Deserialize)]
#[serde(default)]
pub struct Bump {
#[arg(default_value = "patch")]
release: String,
#[arg(short = 'A', long, value_name = "AGENT")]
agent: Option<Vec<String>>,
#[arg(long, value_name = "METADATA")]
build: Option<String>,
#[arg(short = 'd', long)]
dry_run: bool,
#[arg(short = 'k', long)]
no_ask: bool,
#[arg(short = 'P', long, value_name = "PACKAGE")]
package: Option<Vec<String>>,
#[arg(short = 'p', long, value_name = "PATH", default_value = ".")]
path: Option<Vec<PathBuf>>,
#[arg(long, value_name = "IDENTIFIER")]
pre: Option<String>,
}
impl super::Command for Bump {
async fn execute(self) -> Result<()> {
self.set_release()?;
let packages = search_packages!(&self)
.into_iter()
.filter(|it| it.version != DEFAULT_VERSION)
.collect_vec();
preview(&packages);
if self.dry_run {
return Ok(());
}
if self.no_ask {
bump_all(packages).await?;
} else if let PromptResult::Abort = prompt(packages).await? {
return Ok(());
}
Ok(())
}
}
impl Bump {
fn set_release(&self) -> Result<()> {
let mut parser = Release::parser();
if let Some(pre) = self.pre.as_deref() {
parser.prerelease(pre)?;
}
if let Some(build) = self.build.as_deref() {
parser.metadata(build)?;
}
let release = parser.parse(&self.release)?;
RELEASE.set(release).unwrap();
Ok(())
}
}
async fn bump_all(packages: Vec<Package>) -> Result<()> {
let release = RELEASE.get().unwrap();
let agents = packages
.iter()
.map(Package::agent)
.unique()
.collect_vec();
packages
.into_iter()
.try_for_each(|package| package.bump(release))?;
if agents.contains(&Agent::Cargo) {
Command::new("cargo")
.args(["update", "--workspace"])
.spawn()?
.wait()
.await?;
}
Ok(())
}
async fn prompt(mut packages: Vec<Package>) -> Result<PromptResult> {
if packages.len() == 1 {
let package = packages.swap_remove(0);
prompt_one(package)
} else {
prompt_many(packages).await
}
}
fn prompt_one(package: Package) -> Result<PromptResult> {
let message = format!("Bump {}?", package.name);
let should_bump = Confirm::new(&message)
.with_default(true)
.prompt()?;
if should_bump {
let release = RELEASE.get().unwrap();
package.bump(release)?;
Ok(PromptResult::Continue)
} else {
Ok(PromptResult::Abort)
}
}
async fn prompt_many(packages: Vec<Package>) -> Result<PromptResult> {
let options = Choice::iter().collect_vec();
let choice = Select::new("Bump packages?", options).prompt()?;
match choice {
Choice::All => {
bump_all(packages).await?;
Ok(PromptResult::Continue)
}
Choice::Some => {
let message = "Select the packages to bump.";
let packages = packages
.into_iter()
.map(ChoiceWrapper)
.collect();
let packages = MultiSelect::new(message, packages).prompt()?;
if packages.is_empty() {
println!("{}", "no package selected".truecolor(105, 105, 105));
Ok(PromptResult::Abort)
} else {
let packages = packages.into_iter().map(|it| it.0).collect();
bump_all(packages).await?;
Ok(PromptResult::Continue)
}
}
Choice::None => Ok(PromptResult::Abort),
}
}
fn preview(packages: &[Package]) {
use tabled::builder::Builder;
use tabled::settings::object::Segment;
use tabled::settings::{Alignment, Modify, Style};
let release = RELEASE.get().unwrap();
let mut builder = Builder::with_capacity(packages.len(), 5);
for package in packages {
let agent = package
.agent()
.to_string()
.bright_magenta()
.bold();
let version = package
.version
.to_string()
.bright_blue()
.to_string();
let new_version = package
.version
.with_release(release)
.to_string()
.bright_green()
.to_string();
let record = [
agent.to_string(),
package.name.bold().to_string(),
version,
"=>".to_string(),
new_version,
];
builder.push_record(record);
}
let mut table = builder.build();
table.with(Style::blank());
let version_col = Segment::new(.., 2..3);
table.with(Modify::new(version_col).with(Alignment::right()));
let new_version_col = Segment::new(.., 4..5);
table.with(Modify::new(new_version_col).with(Alignment::right()));
println!("{table}");
}
struct ChoiceWrapper(Package);
impl fmt::Display for ChoiceWrapper {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let agent = self.0.agent().to_string();
write!(f, "{agent}: {}", self.0.name)
}
}