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::option::option_display;
use crate::render::spinner::spin_until_ready;
use crate::render::ui::{fuzzy_select_with_key, multi_fuzzy_select_with_key, select_state};
use crate::types::context::BergContext;
use crate::types::git::OwnerRepo;
use crate::types::output::OutputMode;
use forgejo_api::structs::{
    EditPullRequestOption, IssueListLabelsQuery, PullRequest, RepoListPullRequestsQuery,
};
use miette::{Context, IntoDiagnostic};
use strum::{Display, VariantArray};

use crate::actions::text_manipulation::{input_prompt_for, select_prompt_for};

use super::display_pull_request;

use clap::Parser;

/// Edit pull request
#[derive(Parser, Debug)]
pub struct EditPullRequestArgs {}

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

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

        let OwnerRepo { repo, owner } = ctx.owner_repo()?;
        let pull_request = select_pull_request(&ctx).await?;
        let pull_request_id = pull_request
            .number
            .context("Selected pull_request doesn't have an ID")?;

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

        tracing::debug!("{options:?}");
        tracing::debug!("{owner:?}/{repo:?}");
        tracing::debug!("{pull_request_id:?}");

        let updated_pull_request = ctx
            .client
            .repo_edit_pull_request(owner.as_str(), repo.as_str(), pull_request_id, options)
            .await
            .into_diagnostic()?;

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

        Ok(())
    }
}

async fn select_pull_request(
    ctx: &BergContext<EditPullRequestArgs>,
) -> miette::Result<PullRequest> {
    let OwnerRepo { repo, owner } = ctx.owner_repo()?;
    let (_, pull_requests_list) = spin_until_ready(
        ctx.client
            .repo_list_pull_requests(
                owner.as_str(),
                repo.as_str(),
                RepoListPullRequestsQuery::default(),
            )
            .send(),
    )
    .await
    .into_diagnostic()?;

    fuzzy_select_with_key(
        &pull_requests_list,
        select_prompt_for("pull request"),
        display_pull_request,
    )
    .cloned()
}

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

    let mut options = EditPullRequestOption {
        assignee: None,
        assignees: None,
        body: None,
        due_date: None,
        milestone: None,
        state: None,
        title: None,
        unset_due_date: None,
        allow_maintainer_edit: None,
        base: None,
        labels: None,
    };

    if selected_update_fields.contains(&&EditableFields::Assignees) {
        let current_assignees = pull_request
            .assignees
            .as_ref()
            .map(|users| users.iter().filter_map(|user| user.id).collect::<Vec<_>>());
        options
            .assignees
            .replace(pull_request_assignees(ctx, current_assignees).await?);
    }
    if selected_update_fields.contains(&&EditableFields::Description) {
        options
            .body
            .replace(pull_request_description(ctx, pull_request.body.clone()).await?);
    }
    if selected_update_fields.contains(&&EditableFields::State) {
        options.state.replace(select_state(pull_request.state)?);
    }
    if selected_update_fields.contains(&&EditableFields::Title) {
        options
            .title
            .replace(pull_request_title(ctx, pull_request.title.clone()).await?);
    }
    if selected_update_fields.contains(&&EditableFields::Labels) {
        let current_labels = pull_request.labels.as_ref().map(|labels| {
            labels
                .iter()
                .filter_map(|label| label.id)
                .collect::<Vec<_>>()
        });
        options
            .labels
            .replace(pull_request_labels(ctx, current_labels).await?);
    }

    Ok(options)
}

async fn pull_request_assignees(
    ctx: &BergContext<EditPullRequestArgs>,
    current_assignees: Option<Vec<i64>>,
) -> miette::Result<Vec<String>> {
    let OwnerRepo { repo, owner } = ctx.owner_repo()?;
    let current_assignees = current_assignees.unwrap_or_default();
    let (_, all_assignees) = ctx
        .client
        .repo_get_assignees(owner.as_str(), repo.as_str())
        .await
        .into_diagnostic()?;
    let selected_assignees = multi_fuzzy_select_with_key(
        &all_assignees,
        select_prompt_for("assignees"),
        |u| u.id.is_some_and(|id| current_assignees.contains(&id)),
        |u| option_display(&u.login),
    )?;

    Ok(selected_assignees
        .into_iter()
        .filter_map(|u| u.login.as_ref().cloned())
        .collect::<Vec<_>>())
}

async fn pull_request_description(
    ctx: &BergContext<EditPullRequestArgs>,
    current_description: Option<String>,
) -> miette::Result<String> {
    ctx.editor_for(
        "a description",
        current_description
            .as_deref()
            .unwrap_or("Enter a pull request description"),
    )
}

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

async fn pull_request_labels(
    ctx: &BergContext<EditPullRequestArgs>,
    current_labels: Option<Vec<i64>>,
) -> miette::Result<Vec<i64>> {
    let OwnerRepo { repo, owner } = ctx.owner_repo()?;
    let current_labels = current_labels.unwrap_or_default();
    let (_, all_labels) = ctx
        .client
        .issue_list_labels(
            owner.as_str(),
            repo.as_str(),
            IssueListLabelsQuery::default(),
        )
        .await
        .into_diagnostic()?;

    let selected_labels = multi_fuzzy_select_with_key(
        &all_labels,
        select_prompt_for("labels"),
        |l| l.id.is_some_and(|id| current_labels.contains(&id)),
        |l| option_display(&l.name),
    )?;

    let label_ids = selected_labels
        .iter()
        .filter_map(|l| l.id)
        .collect::<Vec<_>>();

    Ok(label_ids)
}