codeberg-cli 0.4.9

CLI Tool for codeberg similar to gh and glab
Documentation
use crate::actions::GeneralArgs;
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::types::context::BergContext;
use crate::types::git::OwnerRepo;
use anyhow::Context;
use forgejo_api::structs::EditMilestoneOption;
use forgejo_api::structs::IssueGetMilestonesListQuery;
use forgejo_api::structs::Milestone;
use forgejo_api::structs::StateType;
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, general_args: GeneralArgs) -> anyhow::Result<()> {
        let _ = general_args;
        let ctx = BergContext::new(self, general_args).await?;

        let OwnerRepo { owner, repo } = ctx.owner_repo()?;
        let milestone = select_milestone(&ctx).await?;
        let milestone_id_or_name = milestone
            .id
            .as_ref()
            .map(|id| id.to_string())
            .or_else(|| milestone.title.clone())
            .context("Selected milestone doesn't have an ID or Name")?;

        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.as_str(),
                options,
            )
            .await?;

        tracing::debug!("{updated_milestone:?}");

        Ok(())
    }
}

async fn select_milestone(ctx: &BergContext<EditMilestoneArgs>) -> anyhow::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(),
    ))
    .await?;

    if milestones_list.is_empty() {
        anyhow::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,
) -> anyhow::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(milestone_state(ctx, current_state).await?);
    }
    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>,
) -> anyhow::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()
        .map_err(anyhow::Error::from)
}

async fn milestone_state(
    _ctx: &BergContext<EditMilestoneArgs>,
    _current_state: Option<StateType>,
) -> anyhow::Result<String> {
    let selected_state = fuzzy_select_with_key(
        &[StateType::Open, StateType::Closed],
        select_prompt_for("states"),
        |f| format!("{f:?}"),
    )
    .cloned()?;
    Ok(format!("{selected_state:?}"))
}

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