codeberg-cli 0.5.5

CLI Tool for codeberg similar to gh and glab
Documentation
use crate::render::comment::render_comment;
use crate::render::datetime::render_datetime_and_info;
use crate::render::json::JsonToStdout;
use crate::render::option::{option_debug_display, option_display};
use crate::render::spinner::spin_until_ready;
use crate::render::ui::fuzzy_select_with_key;
use crate::types::api::state_type::ViewStateType;
use crate::types::context::BergContext;
use crate::types::output::OutputMode;
use crate::{actions::GlobalArgs, types::git::OwnerRepo};
use forgejo_api::structs::{
    Issue, IssueGetCommentsQuery, IssueListIssuesQuery, IssueListIssuesQueryState,
};
use miette::{Context, IntoDiagnostic};

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

use super::display_issue;

use clap::Parser;

/// View details of selected issue
#[derive(Parser, Debug)]
pub struct ViewIssueArgs {
    /// Optionally, specify a selected issue in the command line
    pub index: Option<i64>,

    /// Select from issues with the chosen state
    #[arg(short, long, value_enum, default_value_t = ViewStateType::All)]
    pub state: ViewStateType,

    /// Disabled: display issue summary | Enabled: display issue comment history
    #[arg(short, long)]
    pub comments: bool,
}

impl ViewIssueArgs {
    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 state = match ctx.args.state {
            ViewStateType::Closed => IssueListIssuesQueryState::Closed,
            ViewStateType::Open => IssueListIssuesQueryState::Open,
            ViewStateType::All => IssueListIssuesQueryState::All,
        };
        let selected_issue = if let Some(index) = ctx.args.index {
            ctx.client
                .issue_get_issue(owner.as_str(), repo.as_str(), index)
                .await
                .into_diagnostic()?
        } else {
            if ctx.global_args.non_interactive {
                miette::bail!("non-interactive mode enabled. You have to specify an issue #number");
            };
            let (_, issues_list) = spin_until_ready(
                ctx.client
                    .issue_list_issues(
                        owner.as_str(),
                        repo.as_str(),
                        IssueListIssuesQuery {
                            state: Some(state),
                            ..Default::default()
                        },
                    )
                    .send(),
            )
            .await
            .into_diagnostic()?;

            fuzzy_select_with_key(&issues_list, select_prompt_for("issue"), display_issue)
                .cloned()?
        };

        match ctx.global_args.output_mode {
            OutputMode::Pretty => {
                if ctx.args.comments {
                    spin_until_ready(present_issue_comments(&ctx, &selected_issue)).await?;
                } else {
                    present_issue_overview(&ctx, &selected_issue);
                }
            }
            OutputMode::Json => {
                if ctx.args.comments {
                    let index = selected_issue
                        .number
                        .context("Selected issue has to have a valid issue number")?;
                    let (_, comments) = ctx
                        .client
                        .issue_get_comments(
                            owner.as_str(),
                            repo.as_str(),
                            index,
                            IssueGetCommentsQuery::default(),
                        )
                        .send()
                        .await
                        .into_diagnostic()?;
                    comments.print_json()?;
                } else {
                    selected_issue.print_json()?;
                }
            }
        }

        Ok(())
    }
}

fn present_issue_overview(ctx: &BergContext<ViewIssueArgs>, issue: &Issue) {
    let days_passed_since_creation =
        option_display(&issue.created_at.as_ref().map(render_datetime_and_info));

    let table = ctx
        .make_table()
        .set_header(vec![option_display(
            &issue.id.as_ref().map(|id| format!("Issue #{}", id)),
        )])
        .add_row(vec![String::from("Title"), option_display(&issue.title)])
        .add_row(vec![String::from("Created"), days_passed_since_creation])
        .add_row(vec![
            String::from("Labels"),
            option_display(&issue.labels.as_ref().map(|labels| {
                labels
                    .iter()
                    .map(|label| option_display(&label.name))
                    .collect::<Vec<_>>()
                    .join(", ")
            })),
        ])
        .add_row(vec![
            String::from("State"),
            option_debug_display(&issue.state),
        ])
        .add_row(vec![
            String::from("Description"),
            option_display(&issue.body),
        ]);

    println!("{table}", table = table.show());
}

async fn present_issue_comments(
    ctx: &BergContext<ViewIssueArgs>,
    issue: &Issue,
) -> miette::Result<()> {
    let index = issue.number.context("Issue has no issue number")?;
    let OwnerRepo { repo, owner } = ctx.owner_repo()?;
    let (header, comments) = {
        let (_, comments_list) = ctx
            .client
            .issue_get_comments(
                owner.as_str(),
                repo.as_str(),
                index,
                IssueGetCommentsQuery::default(),
            )
            .send()
            .await
            .into_diagnostic()?;
        let header = format!(
            "Issue #{} {}",
            index,
            if comments_list.is_empty() {
                "(no comments)"
            } else {
                "comments"
            }
        );
        (header, comments_list)
    };

    let table = ctx
        .make_table()
        .add_row(vec![header])
        .add_rows(comments.into_iter().filter_map(|comment| {
            let username = comment.user.as_ref()?.login.as_ref()?.as_str();
            let creation_time = comment.created_at.as_ref()?;
            let comment = comment.body.as_ref()?;
            let comment = render_comment(username, creation_time, comment);
            Some(vec![comment])
        }));

    println!("{table}", table = table.show());

    Ok(())
}