codeberg-cli 0.4.9

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

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

        Ok(())
    }
}

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

    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,
) -> anyhow::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(pull_request_state(ctx, 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>>,
) -> anyhow::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?;
    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>,
) -> anyhow::Result<String> {
    ctx.editor_for(
        "a description",
        current_description
            .as_deref()
            .unwrap_or("Enter a pull request description"),
    )
}

fn pull_request_state(
    _ctx: &BergContext<EditPullRequestArgs>,
    _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:?}"),
    )?;
    Ok(format!("{selected_state:?}"))
}

async fn pull_request_title(
    _ctx: &BergContext<EditPullRequestArgs>,
    current_title: Option<String>,
) -> anyhow::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()
        .map_err(anyhow::Error::from)
}

async fn pull_request_labels(
    ctx: &BergContext<EditPullRequestArgs>,
    current_labels: Option<Vec<i64>>,
) -> anyhow::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?;

    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)
}