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::spinner::spin_until_ready;
use crate::render::ui::fuzzy_select_with_key;
use crate::render::ui::multi_fuzzy_select_with_key;
use crate::render::ui::select_state;
use crate::types::context::BergContext;
use crate::types::git::OwnerRepo;
use crate::types::output::OutputMode;
use forgejo_api::structs::EditMilestoneOption;
use forgejo_api::structs::IssueGetMilestonesListQuery;
use forgejo_api::structs::Milestone;
use miette::Context;
use miette::IntoDiagnostic;
use strum::Display;
use strum::VariantArray;

use crate::actions::text_manipulation::input_prompt_for;
use crate::actions::text_manipulation::select_prompt_for;

use super::display_milestone;

use clap::Parser;

/// Edit selected issue
#[derive(Parser, Debug)]
pub struct EditMilestoneArgs {}

#[derive(Display, PartialEq, Eq, VariantArray)]
enum EditableFields {
    Title,
    State,
    Description,
}

impl EditMilestoneArgs {
    pub async fn run(self, global_args: GlobalArgs) -> miette::Result<()> {
        let ctx = BergContext::new(self, global_args).await?;

        let OwnerRepo { owner, repo } = ctx.owner_repo()?;
        let milestone = select_milestone(&ctx).await?;
        let milestone_id_or_name = milestone
            .id
            .context("Selected milestone doesn't have an ID")?;

        let options = create_options(&ctx, &milestone).await?;

        let updated_milestone = ctx
            .client
            .issue_edit_milestone(owner.as_str(), repo.as_str(), milestone_id_or_name, options)
            .await
            .into_diagnostic()?;

        match ctx.global_args.output_mode {
            OutputMode::Pretty => {
                tracing::debug!("{updated_milestone:?}");
            }
            OutputMode::Json => updated_milestone.print_json()?,
        }

        Ok(())
    }
}

async fn select_milestone(ctx: &BergContext<EditMilestoneArgs>) -> miette::Result<Milestone> {
    let OwnerRepo { owner, repo } = ctx.owner_repo()?;
    let (_, milestones_list) = spin_until_ready(
        ctx.client
            .issue_get_milestones_list(
                owner.as_str(),
                repo.as_str(),
                IssueGetMilestonesListQuery::default(),
            )
            .send(),
    )
    .await
    .into_diagnostic()?;

    if milestones_list.is_empty() {
        miette::bail!("No milestones found in this repository");
    }

    fuzzy_select_with_key(
        &milestones_list,
        select_prompt_for("milestone"),
        display_milestone,
    )
    .cloned()
}

async fn create_options(
    ctx: &BergContext<EditMilestoneArgs>,
    milestone: &Milestone,
) -> miette::Result<EditMilestoneOption> {
    let selected_update_fields = multi_fuzzy_select_with_key(
        EditableFields::VARIANTS,
        select_prompt_for("options"),
        |_| false,
        |f| f.to_string(),
    )?;

    let mut options = EditMilestoneOption {
        description: None,
        due_on: None,
        state: None,
        title: None,
    };

    if selected_update_fields.contains(&&EditableFields::Title) {
        let current_title = milestone.title.as_ref().cloned();
        options
            .title
            .replace(milestone_title(ctx, current_title).await?);
    }
    if selected_update_fields.contains(&&EditableFields::State) {
        let current_state = milestone.state.as_ref().cloned();
        options.state.replace(select_state(current_state)?);
    }
    if selected_update_fields.contains(&&EditableFields::Description) {
        let current_description = milestone.description.as_ref().cloned();
        options
            .description
            .replace(milestone_description(ctx, current_description).await?);
    }

    Ok(options)
}

async fn milestone_title(
    _ctx: &BergContext<EditMilestoneArgs>,
    current_title: Option<String>,
) -> miette::Result<String> {
    inquire::Text::new(input_prompt_for("Choose a new milestone title").as_str())
        .with_default(
            current_title
                .as_deref()
                .unwrap_or("Enter a milestone title"),
        )
        .prompt()
        .into_diagnostic()
}

async fn milestone_description(
    ctx: &BergContext<EditMilestoneArgs>,
    current_description: Option<String>,
) -> miette::Result<String> {
    ctx.editor_for(
        "a description",
        current_description
            .as_deref()
            .unwrap_or("Enter a milestone description"),
    )
}