use crate::actions::GlobalArgs;
use crate::render::json::JsonToStdout;
use crate::render::ui::{fuzzy_select_with_key, multi_fuzzy_select_with_key};
use crate::types::context::BergContext;
use crate::types::git::OwnerRepo;
use forgejo_api::structs::CreateReleaseOption;
use itertools::Itertools;
use miette::{Context, IntoDiagnostic};
use strum::*;
use crate::actions::text_manipulation::{input_prompt_for, select_prompt_for};
use clap::Parser;
#[derive(Parser, Debug)]
pub struct CreateReleaseArgs {
#[arg(id = "description", short, long)]
pub body: Option<String>,
#[arg(short, long)]
pub name: Option<String>,
#[arg(short, long)]
pub tag: Option<String>,
}
#[derive(Display, PartialEq, Eq, VariantArray)]
enum CreatableFields {
Body,
Name,
}
impl CreateReleaseArgs {
pub async fn run(self, global_args: GlobalArgs) -> miette::Result<()> {
let _ = global_args;
let ctx = BergContext::new(self, global_args).await?;
let OwnerRepo { repo, owner } = ctx.owner_repo()?;
let options = create_options(&ctx).await?;
let tag_str = format!(
"{name} ({commit})",
name = options.name.as_ref().map_or("", |v| v),
commit = options.tag_name
);
let release = ctx
.client
.repo_create_release(owner.as_str(), repo.as_str(), options)
.await
.into_diagnostic()?;
match ctx.global_args.output_mode {
crate::types::output::OutputMode::Pretty => {
tracing::debug!("{release:?}");
println!("Successfully released {tag_str}");
}
crate::types::output::OutputMode::Json => {
release.print_json()?;
}
}
Ok(())
}
}
async fn create_options(
ctx: &BergContext<CreateReleaseArgs>,
) -> miette::Result<CreateReleaseOption> {
let tag_name = if ctx.global_args.non_interactive {
ctx.args
.tag
.clone()
.context("You have to specify a valid tag name in non-interactive mode!")?
} else {
release_tag(ctx).await?
};
let mut options = CreateReleaseOption {
tag_name,
body: ctx.args.body.clone(),
draft: None,
hide_archive_links: None,
name: ctx.args.name.clone(),
prerelease: None,
target_commitish: None,
};
if !ctx.global_args.non_interactive {
let optional_data = {
use CreatableFields::*;
[
(Body, ctx.args.body.is_none()),
(Name, ctx.args.name.is_none()),
]
.into_iter()
.filter_map(|(name, missing)| missing.then_some(name))
.collect::<Vec<_>>()
};
if !optional_data.is_empty() {
let chosen_optionals = multi_fuzzy_select_with_key(
&optional_data,
"Choose optional properties",
|_| false,
|o| o.to_string(),
)
.context("No optional fields exist that need to be set!")?;
{
use CreatableFields::*;
options.body = release_body(ctx, chosen_optionals.contains(&&Body)).await?;
options.name = release_name(ctx, chosen_optionals.contains(&&Name)).await?;
}
}
}
Ok(options)
}
async fn release_tag(ctx: &BergContext<CreateReleaseArgs>) -> miette::Result<String> {
let OwnerRepo { repo, owner } = ctx.owner_repo()?;
let (_, all_tags) = ctx
.client
.repo_list_tags(owner.as_str(), repo.as_str())
.await
.into_diagnostic()?;
let (version_tags, non_version_tags): (Vec<_>, Vec<_>) = all_tags
.iter()
.filter(|tag| tag.name.as_ref().is_some())
.partition(|tag| tag.name.as_ref().is_some_and(|name| name.starts_with("v")));
let all_tags = version_tags
.into_iter()
.sorted_by_key(|tag| tag.name.as_ref().map_or("", |v| v))
.chain(
non_version_tags
.into_iter()
.sorted_by_key(|tag| tag.name.as_ref().map_or("", |v| v)),
)
.collect::<Vec<_>>();
miette::ensure!(
!all_tags.is_empty(),
"You need to push some at least one tag manually before releasing!"
);
let tag = match &ctx.args.tag {
Some(tag_name) => all_tags
.iter()
.find(|t| t.name.as_ref().is_some_and(|name| name == tag_name))
.and_then(|tag| tag.id.clone())
.context(format!(
"Tag with name {tag_name} wasn't found. Check the spelling"
))?,
None => {
let selected_tag = fuzzy_select_with_key(&all_tags, select_prompt_for("tags"), |t| {
t.name
.as_ref()
.cloned()
.unwrap_or_else(|| String::from("???"))
})
.context("No tags exist!")?;
selected_tag
.id
.clone()
.context("Selected tag's id is missing")?
}
};
Ok(tag)
}
async fn release_body(
ctx: &BergContext<CreateReleaseArgs>,
interactive: bool,
) -> miette::Result<Option<String>> {
let body = match ctx.args.body.as_ref() {
Some(body) => body.clone(),
None => {
if !interactive {
return Ok(None);
}
ctx.editor_for("a description", "Enter a release description")?
}
};
Ok(Some(body))
}
async fn release_name(
ctx: &BergContext<CreateReleaseArgs>,
interactive: bool,
) -> miette::Result<Option<String>> {
let name = match ctx.args.name.as_ref() {
Some(name) => name.clone(),
None => {
if !interactive {
return Ok(None);
}
inquire::Text::new(input_prompt_for("Release Name").as_str())
.prompt()
.into_diagnostic()?
}
};
Ok(Some(name))
}