xbp 10.39.0

XBP is a zero-config build pack that can also interact with proxies, kafka, sockets, synthetic monitors.
//! Interactive GitHub issues hub.

use super::client::{
    get_issue, list_comments, list_issues, print_issue_detail, print_issue_table, run_assign,
    run_comment, run_create, run_labels, run_status, ListIssuesFilter,
};
use super::require_interactive;
use crate::cli::commands::{
    GithubAssignCmd, GithubCommentCmd, GithubCreateCmd, GithubLabelsCmd, GithubStatusCmd,
};
use crate::cli::ui::Loader;
use colored::Colorize;
use dialoguer::{theme::ColorfulTheme, FuzzySelect, Input, Select};

pub async fn run_hub(token: &str, owner: &str, repo: &str) -> Result<(), String> {
    require_interactive("GitHub interactive hub")?;
    println!(
        "{} {}/{}",
        "GitHub issues".bright_cyan().bold(),
        owner,
        repo
    );
    loop {
        println!();
        let choices = [
            "List issues",
            "Create issue",
            "Open issue by number",
            "Scan TODOs (xbp issues)",
            "Exit",
        ];
        let idx = Select::with_theme(&ColorfulTheme::default())
            .with_prompt("What do you want to do?")
            .items(&choices)
            .default(0)
            .interact()
            .map_err(|e| e.to_string())?;
        match idx {
            0 => list_and_act(token, owner, repo).await?,
            1 => {
                run_create(
                    token,
                    owner,
                    repo,
                    GithubCreateCmd {
                        title: None,
                        body: None,
                        labels: Vec::new(),
                        assignees: Vec::new(),
                        interactive_body: true,
                    },
                )
                .await?;
            }
            2 => {
                let id: String = Input::with_theme(&ColorfulTheme::default())
                    .with_prompt("Issue number")
                    .interact_text()
                    .map_err(|e| e.to_string())?;
                if !id.trim().is_empty() {
                    open_actions(token, owner, repo, id.trim()).await?;
                }
            }
            3 => {
                println!(
                    "{}",
                    "Run `xbp issues scan` then `xbp issues sync --to github`.".dimmed()
                );
                let _ = crate::commands::todos::run_todos_scan_default().await;
            }
            _ => break,
        }
    }
    Ok(())
}

async fn list_and_act(token: &str, owner: &str, repo: &str) -> Result<(), String> {
    let loader = Loader::start("Fetching issues");
    let issues = match list_issues(
        token,
        owner,
        repo,
        ListIssuesFilter {
            state: "open".into(),
            limit: 40,
            ..Default::default()
        },
    )
    .await
    {
        Ok(v) => {
            loader.success();
            v
        }
        Err(e) => {
            loader.fail(&e);
            return Err(e);
        }
    };
    print_issue_table(&issues);
    if issues.is_empty() {
        return Ok(());
    }
    let mut labels: Vec<String> = issues
        .iter()
        .map(|i| format!("#{}{}", i.number, i.title))
        .collect();
    labels.push("(back)".into());
    let idx = FuzzySelect::with_theme(&ColorfulTheme::default())
        .with_prompt("Open issue")
        .items(&labels)
        .default(0)
        .interact()
        .map_err(|e| e.to_string())?;
    if idx >= issues.len() {
        return Ok(());
    }
    open_actions(token, owner, repo, &issues[idx].number.to_string()).await
}

async fn open_actions(token: &str, owner: &str, repo: &str, id: &str) -> Result<(), String> {
    loop {
        let number = super::client::parse_issue_number(id)?;
        let issue = get_issue(token, owner, repo, number).await?;
        print_issue_detail(&issue);
        let actions = [
            "Change state",
            "Edit labels",
            "Assign",
            "View comments",
            "Add comment",
            "Back",
        ];
        let idx = Select::with_theme(&ColorfulTheme::default())
            .with_prompt(format!("Actions for #{}", issue.number))
            .items(&actions)
            .default(0)
            .interact()
            .map_err(|e| e.to_string())?;
        match idx {
            0 => {
                run_status(
                    token,
                    owner,
                    repo,
                    GithubStatusCmd {
                        id: issue.number.to_string(),
                        state: None,
                    },
                )
                .await?;
            }
            1 => {
                run_labels(
                    token,
                    owner,
                    repo,
                    GithubLabelsCmd {
                        id: issue.number.to_string(),
                        add: Vec::new(),
                        remove: Vec::new(),
                        set: Vec::new(),
                        clear: false,
                    },
                )
                .await?;
            }
            2 => {
                run_assign(
                    token,
                    owner,
                    repo,
                    GithubAssignCmd {
                        id: issue.number.to_string(),
                        assignee: None,
                    },
                )
                .await?;
            }
            3 => {
                let comments = list_comments(token, owner, repo, number).await?;
                if comments.is_empty() {
                    println!("{}", "No comments.".dimmed());
                } else {
                    for c in comments {
                        println!(
                            "{}{}",
                            c.user_login.as_deref().unwrap_or("?").bright_cyan(),
                            c.body.replace('\n', " ")
                        );
                    }
                }
            }
            4 => {
                run_comment(
                    token,
                    owner,
                    repo,
                    GithubCommentCmd {
                        id: issue.number.to_string(),
                        body: None,
                    },
                )
                .await?;
            }
            _ => break,
        }
    }
    Ok(())
}