5.7 KiB · text History b868907
//! Issues: list, read, comment, open and close.

use anyhow::Result;

use crate::cli::IssueCommand;
use crate::commands::{Ctx, read_body};
use crate::output::{
    Table, ago, author_of, bold, heading, indented, labels, note, state_cell, truncate,
};

/// "No open issues" reads; "No all issues" doesn't.
fn qualifier(state: &str) -> String {
    if state == "all" {
        String::new()
    } else {
        format!("{state} ")
    }
}

pub async fn run(ctx: &Ctx, command: &IssueCommand) -> Result<()> {
    let repo = ctx.repo()?;

    match command {
        IssueCommand::List {
            state,
            search,
            limit,
        } => {
            let res = ctx
                .api
                .issues(&repo, Some(state.as_param()), search.as_deref(), *limit)
                .await?;
            if ctx.emitted(&res.raw)? {
                return Ok(());
            }
            if res.issues.is_empty() {
                note(&format!(
                    "No {}issues in {repo}.",
                    qualifier(state.as_param())
                ));
                return Ok(());
            }

            let mut table = Table::new(["#", "STATE", "TITLE", "AUTHOR", "LABELS", "UPDATED"]);
            for issue in &res.issues {
                table.row([
                    format!("#{}", issue.number),
                    state_cell(&issue.state),
                    truncate(issue.title.as_deref().unwrap_or("(untitled)"), 52),
                    author_of(issue.author.as_ref()),
                    truncate(&labels(&issue.labels), 24),
                    ago(issue.updated_at.as_deref().or(issue.inserted_at.as_deref())),
                ]);
            }
            table.print();
            Ok(())
        }

        IssueCommand::Show {
            number,
            no_comments,
        } => {
            let res = ctx.api.issue(&repo, *number).await?;
            if ctx.emitted(&res.raw)? {
                return Ok(());
            }

            let issue = &res.issue;
            heading(&format!(
                "#{} {}",
                issue.number,
                issue.title.as_deref().unwrap_or("(untitled)")
            ));
            println!(
                "{} · opened by {} {}{}",
                state_cell(&issue.state),
                author_of(issue.author.as_ref()),
                ago(issue.inserted_at.as_deref()),
                if issue.labels.is_empty() {
                    String::new()
                } else {
                    format!(" · {}", labels(&issue.labels))
                }
            );

            if let Some(body) = issue.body.as_deref().filter(|b| !b.trim().is_empty()) {
                println!();
                indented(body, "  ");
            }

            if !no_comments && !res.comments.is_empty() {
                println!("\n{}", bold(&format!("{} comment(s)", res.comments.len())));
                for comment in &res.comments {
                    println!();
                    println!(
                        "{} {}{}",
                        bold(&author_of(comment.author.as_ref())),
                        ago(comment.inserted_at.as_deref()),
                        if comment.edited_at.is_some() {
                            " (edited)"
                        } else {
                            ""
                        }
                    );
                    let body = if comment.deleted {
                        "(deleted)"
                    } else {
                        comment.body.as_deref().unwrap_or("")
                    };
                    indented(body, "  ");
                }
            }

            note(&format!(
                "\n{}",
                ctx.repo_web_url(&repo, &format!("/issues/{}", issue.number))
            ));
            if !res.can_comment {
                note("(read-only: this token can't comment here)");
            }
            Ok(())
        }

        IssueCommand::Create {
            title,
            body,
            labels,
        } => {
            let text = read_body(body.body.as_deref(), body.body_file.as_deref(), false)?;
            let res = ctx
                .api
                .create_issue(
                    &repo,
                    title,
                    text.as_deref().map(str::trim).filter(|t| !t.is_empty()),
                    labels,
                )
                .await?;
            if ctx.emitted(&res.raw)? {
                return Ok(());
            }
            println!(
                "Opened issue #{}: {}",
                res.issue.number,
                res.issue.title.as_deref().unwrap_or("(untitled)")
            );
            println!(
                "{}",
                ctx.repo_web_url(&repo, &format!("/issues/{}", res.issue.number))
            );
            Ok(())
        }

        IssueCommand::Comment { number, body } => {
            let text = read_body(body.body.as_deref(), body.body_file.as_deref(), true)?
                .expect("required body");
            let res = ctx
                .api
                .comment_on_issue(&repo, *number, text.trim())
                .await?;
            if ctx.emitted(&res.raw)? {
                return Ok(());
            }
            println!("Commented on issue #{number}.");
            Ok(())
        }

        IssueCommand::Close { number } => set_state(ctx, &repo, *number, "closed").await,
        IssueCommand::Reopen { number } => set_state(ctx, &repo, *number, "open").await,
    }
}

async fn set_state(ctx: &Ctx, repo: &crate::api::RepoRef, number: i64, state: &str) -> Result<()> {
    let res = ctx.api.set_issue_state(repo, number, state).await?;
    if ctx.emitted(&res.raw)? {
        return Ok(());
    }
    println!("Issue #{number} is now {}.", state_cell(state));
    Ok(())
}