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