codeberg-cli 0.5.5

CLI Tool for codeberg similar to gh and glab
Documentation
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;

/// Create a release
#[derive(Parser, Debug)]
pub struct CreateReleaseArgs {
    /// Main description of release
    #[arg(id = "description", short, long)]
    pub body: Option<String>,

    /// Release name
    #[arg(short, long)]
    pub name: Option<String>,

    /// Name of the tag to be released
    #[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()?;

    // we want to show tags with a "v" prefix first
    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))
}