use std::{
fs::File,
path::{Path, PathBuf},
};
use anyhow::{Context, Error};
use clap::{builder::TypedValueParser, crate_version, Parser};
use log::Level;
mod logger;
use semantic_release_cargo::{
list_packages_with_arguments, prepare, publish, verify_conditions_with_alternate, PublishArgs,
};
#[derive(Parser)]
#[clap(version = crate_version!())]
struct Opt {
#[clap(short, long, group = "logging", action = clap::ArgAction::Count)]
verbose: u8,
#[clap(
short,
long,
group = "logging",
value_parser = clap::builder::PossibleValuesParser::new(["error", "warn", "info", "debug", "trace"])
.map(|s| s.parse::<log::Level>().unwrap()),
)]
log_level: Option<Level>,
#[structopt(short, long)]
output: Option<PathBuf>,
#[clap(subcommand)]
subcommand: Subcommand,
}
#[derive(Parser)]
enum Subcommand {
ListPackages(CommonOpt),
#[clap(verbatim_doc_comment)]
VerifyConditions(CommonOpt),
Prepare(PrepareOpt),
Publish(PublishOpt),
}
#[derive(Parser)]
struct CommonOpt {
#[clap(long)]
manifest_path: Option<PathBuf>,
#[clap(long)]
registry: Option<String>,
}
#[derive(Parser)]
struct PrepareOpt {
#[clap(flatten)]
common: CommonOpt,
next_version: String,
}
#[derive(Parser)]
struct PublishOpt {
#[clap(flatten)]
common: CommonOpt,
#[clap(long)]
no_dirty: bool,
#[clap(long, value_parser = parse_key_val::<String, String>, value_delimiter = ',')]
features: Vec<(String, String)>,
}
fn parse_key_val<T, U>(
s: &str,
) -> Result<(T, U), Box<dyn std::error::Error + Send + Sync + 'static>>
where
T: std::str::FromStr,
T::Err: std::error::Error + Send + Sync + 'static,
U: std::str::FromStr,
U::Err: std::error::Error + Send + Sync + 'static,
{
let pos = s
.find('=')
.ok_or_else(|| format!("invalid KEY=value: no `=` found in `{}`", s))?;
Ok((s[..pos].parse()?, s[pos + 1..].parse()?))
}
impl Subcommand {
fn run(&self) -> Result<(), Error> {
use Subcommand::*;
match self {
ListPackages(opt) => Ok(list_packages_with_arguments(
opt.registry.as_deref(),
opt.manifest_path(),
)?),
VerifyConditions(opt) => Ok(verify_conditions_with_alternate(
opt.registry.as_deref(),
opt.manifest_path(),
)?),
Prepare(opt) => Ok(prepare(
opt.common.manifest_path(),
opt.next_version.clone(),
)?),
Publish(opt) => Ok(publish(
opt.common.manifest_path(),
&PublishArgs {
no_dirty: Some(opt.no_dirty),
features: Some(opt.features.iter().cloned().fold(
Default::default(),
|mut a, (k, v)| {
a.entry(k).or_default().push(v);
a
},
)),
registry: opt.common.registry.clone(),
},
)?),
}
}
}
fn main() -> Result<(), Error> {
let opt: Opt = Opt::parse();
let log_builder = logger::LoggerBuilder::default()
.output(Level::Trace, std::io::stderr())
.output(Level::Debug, std::io::stderr());
let log_builder = if let Some(log_level) = opt.log_level {
log_builder.max_level(log_level)
} else {
log_builder.verbosity(opt.verbose)
};
match opt.output {
Some(path) => {
let file = File::create(&path)
.with_context(|| format!("Failed to create output file {}", path.display()))?;
let log_builder_with_output = log_builder.append_output(Level::Info, file);
let boxed_logger = log_builder_with_output.finalize()?;
let max_level_filter = boxed_logger.max_level_filter();
log::set_boxed_logger(boxed_logger)
.map(|()| log::set_max_level(max_level_filter))
.map_err(|_| logger::Error::Initialization)?;
opt.subcommand.run()
}
None => {
let boxed_logger = log_builder.finalize()?;
let max_level_filter = boxed_logger.max_level_filter();
log::set_boxed_logger(boxed_logger)
.map(|()| log::set_max_level(max_level_filter))
.map_err(|_| logger::Error::Initialization)?;
opt.subcommand.run()
}
}
}
impl CommonOpt {
fn manifest_path(&self) -> Option<&Path> {
self.manifest_path.as_deref()
}
}