use crate::actions::GlobalArgs;
use crate::render::json::JsonToStdout;
use crate::render::ui::{fuzzy_select_with_key, multi_fuzzy_select_with_key};
use crate::types::context::BergContext;
use crate::types::git::OwnerRepo;
use crate::types::output::OutputMode;
use forgejo_api::structs::{CreateIssueOption, IssueGetMilestonesListQuery, IssueListLabelsQuery};
use miette::{Context, IntoDiagnostic};
use strum::*;
use crate::actions::text_manipulation::{input_prompt_for, select_prompt_for};
use clap::Parser;
#[derive(Parser, Debug)]
pub struct CreateIssueArgs {
#[arg(short, long)]
pub title: Option<String>,
#[arg(short, long, value_name = "LABEL,...", value_delimiter = ',')]
pub labels: Option<Vec<String>>,
#[arg(id = "description", short, long)]
pub body: Option<String>,
#[arg(short, long, value_name = "ASSIGNEE,...", value_delimiter = ',')]
pub assignees: Option<Vec<String>>,
#[arg(short, long)]
pub milestone: Option<String>,
}
#[derive(Display, PartialEq, Eq, VariantArray)]
enum CreatableFields {
Labels,
Assignees,
Milestone,
}
impl CreateIssueArgs {
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 options = create_options(&ctx).await?;
tracing::debug!("{options:?}");
let issue = ctx
.client
.issue_create_issue(owner.as_str(), repo.as_str(), options)
.await
.into_diagnostic()?;
match ctx.global_args.output_mode {
OutputMode::Pretty => {
tracing::debug!("{issue:?}");
}
OutputMode::Json => issue.print_json()?,
}
Ok(())
}
}
async fn create_options(ctx: &BergContext<CreateIssueArgs>) -> miette::Result<CreateIssueOption> {
let OwnerRepo { repo, owner } = ctx.owner_repo()?;
let title = ctx
.args
.title
.as_ref()
.cloned()
.map(Ok)
.unwrap_or_else(|| {
if ctx.global_args.non_interactive {
miette::bail!("You have to set an issue title!");
} else {
inquire::Text::new(input_prompt_for("Issue Title").as_str())
.prompt()
.into_diagnostic()
}
})?;
let milestone = if let Some(milestone_title) = ctx.args.milestone.as_ref() {
let (_, milestones) = ctx
.client
.issue_get_milestones_list(owner.as_str(), repo.as_str(), Default::default())
.await
.into_diagnostic()?;
let selected_milestone = milestones
.into_iter()
.find(|milestone| {
milestone
.title
.as_ref()
.is_some_and(|title| title == milestone_title)
})
.with_context(|| format!("Couldn't find milestone with title: {milestone_title}"))?;
Some(
selected_milestone
.id
.context("Milestone is expected to have a milestone ID!")?,
)
} else {
None
};
let labels = if let Some(requested_labels) = ctx.args.labels.as_ref() {
let (_, issue_labels) = ctx
.client
.issue_list_labels(owner.as_str(), repo.as_str(), Default::default())
.await
.into_diagnostic()?;
let labels = requested_labels
.iter()
.map(|label_name| {
issue_labels
.iter()
.find(|label| label.name.as_ref().is_some_and(|name| name == label_name))
.with_context(|| format!("Label with name {label_name} doesn't exist yet"))
.context("Please create all labels before using them")?
.id
.context("Selected label is missing an ID")
})
.collect::<Result<Vec<_>, _>>()?;
Some(labels)
} else {
None
};
let mut options = CreateIssueOption {
title,
assignee: None,
assignees: ctx.args.assignees.clone(),
body: ctx.args.body.clone(),
closed: None,
due_date: None,
labels,
milestone,
r#ref: None,
};
options.body.replace(issue_body(ctx)?);
if !ctx.global_args.non_interactive {
let optional_data = {
use CreatableFields::*;
[
(Labels, ctx.args.labels.is_none()),
(Assignees, ctx.args.assignees.is_none()),
(Milestone, ctx.args.milestone.is_none()),
]
.into_iter()
.filter_map(|(name, missing)| missing.then_some(name))
.collect::<Vec<_>>()
};
let chosen_optionals = multi_fuzzy_select_with_key(
&optional_data,
"Choose optional properties",
|_| false,
|o| o.to_string(),
)?;
{
use CreatableFields::*;
options.labels = issue_labels(ctx, chosen_optionals.contains(&&Labels)).await?;
options.assignees =
issue_assignees(ctx, chosen_optionals.contains(&&Assignees)).await?;
options.milestone =
issue_milestone(ctx, chosen_optionals.contains(&&Milestone)).await?;
}
}
Ok(options)
}
fn issue_body(ctx: &BergContext<CreateIssueArgs>) -> miette::Result<String> {
let body = match ctx.args.body.as_ref() {
Some(body) => body.clone(),
None => {
if ctx.global_args.non_interactive {
miette::bail!("You need to provide an issue description!");
} else {
ctx.editor_for("a description", "Enter an issue description")?
}
}
};
Ok(body)
}
async fn issue_labels(
ctx: &BergContext<CreateIssueArgs>,
interactive: bool,
) -> miette::Result<Option<Vec<i64>>> {
let OwnerRepo { repo, owner } = ctx.owner_repo()?;
let (_, all_labels) = ctx
.client
.issue_list_labels(
owner.as_str(),
repo.as_str(),
IssueListLabelsQuery::default(),
)
.await
.into_diagnostic()?;
let labels = match &ctx.args.labels {
Some(label_names) => {
let label_ids = all_labels
.iter()
.filter(|l| {
l.name
.as_ref()
.is_some_and(|name| label_names.contains(name))
})
.filter_map(|l| l.id)
.collect::<Vec<_>>();
Some(label_ids)
}
None => {
if !interactive {
return Ok(None);
}
if all_labels.is_empty() {
println!(
"No labels available! You might want to create some via `berg label create`!"
);
return Ok(None);
}
let selected_labels = multi_fuzzy_select_with_key(
&all_labels,
select_prompt_for("labels"),
|_| false,
|l| {
l.name
.as_ref()
.cloned()
.unwrap_or_else(|| String::from("???"))
},
)?;
let label_ids = selected_labels
.iter()
.filter_map(|l| l.id)
.collect::<Vec<_>>();
Some(label_ids)
}
};
Ok(labels)
}
async fn issue_assignees(
ctx: &BergContext<CreateIssueArgs>,
interactive: bool,
) -> miette::Result<Option<Vec<String>>> {
let OwnerRepo { repo, owner } = ctx.owner_repo()?;
let (_, all_assignees) = ctx
.client
.repo_get_assignees(owner.as_str(), repo.as_str())
.await
.into_diagnostic()?;
let assignees = match &ctx.args.assignees {
Some(assignees_names) => Some(assignees_names.clone()),
None => {
tracing::debug!("Hey");
if !interactive {
return Ok(None);
}
let selected_assignees = multi_fuzzy_select_with_key(
&all_assignees,
select_prompt_for("assignees"),
|_| false,
|u| {
u.login
.as_ref()
.cloned()
.unwrap_or_else(|| String::from("???"))
},
)?;
Some(
selected_assignees
.into_iter()
.filter_map(|u| u.login.as_ref().cloned())
.collect::<Vec<_>>(),
)
}
};
Ok(assignees)
}
async fn issue_milestone(
ctx: &BergContext<CreateIssueArgs>,
interactive: bool,
) -> miette::Result<Option<i64>> {
let OwnerRepo { repo, owner } = ctx.owner_repo()?;
let (_, all_milestones) = ctx
.client
.issue_get_milestones_list(
owner.as_str(),
repo.as_str(),
IssueGetMilestonesListQuery::default(),
)
.await
.into_diagnostic()?;
let milestone = match &ctx.args.milestone {
Some(milestone_name) => Some(
all_milestones
.iter()
.find(|m| m.title.as_ref().is_some_and(|name| name == milestone_name))
.and_then(|milestone| milestone.id)
.context(format!(
"Milestone with name {milestone_name} wasn't found. Check the spelling"
))?,
),
None => {
if !interactive {
return Ok(None);
}
let selected_milestone =
fuzzy_select_with_key(&all_milestones, select_prompt_for("milestones"), |m| {
m.title
.as_ref()
.cloned()
.unwrap_or_else(|| String::from("???"))
})?;
Some(
selected_milestone
.id
.context("Selected milestone's id is missing")?,
)
}
};
Ok(milestone)
}