use crate::actions::GlobalArgs;
use crate::render::json::JsonToStdout;
use crate::render::option::option_display;
use crate::render::spinner::spin_until_ready;
use crate::render::ui::{fuzzy_select_with_key, multi_fuzzy_select_with_key, select_state};
use crate::types::context::BergContext;
use crate::types::git::OwnerRepo;
use crate::types::output::OutputMode;
use forgejo_api::structs::{
EditPullRequestOption, IssueListLabelsQuery, PullRequest, RepoListPullRequestsQuery,
};
use miette::{Context, IntoDiagnostic};
use strum::{Display, VariantArray};
use crate::actions::text_manipulation::{input_prompt_for, select_prompt_for};
use super::display_pull_request;
use clap::Parser;
#[derive(Parser, Debug)]
pub struct EditPullRequestArgs {}
#[derive(Display, PartialEq, Eq, VariantArray)]
enum EditableFields {
Assignees,
Description,
State,
Labels,
Title,
}
impl EditPullRequestArgs {
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 pull_request = select_pull_request(&ctx).await?;
let pull_request_id = pull_request
.number
.context("Selected pull_request doesn't have an ID")?;
let options = create_options(&ctx, &pull_request).await?;
tracing::debug!("{options:?}");
tracing::debug!("{owner:?}/{repo:?}");
tracing::debug!("{pull_request_id:?}");
let updated_pull_request = ctx
.client
.repo_edit_pull_request(owner.as_str(), repo.as_str(), pull_request_id, options)
.await
.into_diagnostic()?;
match ctx.global_args.output_mode {
OutputMode::Pretty => {
tracing::debug!("{updated_pull_request:?}");
}
OutputMode::Json => updated_pull_request.print_json()?,
}
Ok(())
}
}
async fn select_pull_request(
ctx: &BergContext<EditPullRequestArgs>,
) -> miette::Result<PullRequest> {
let OwnerRepo { repo, owner } = ctx.owner_repo()?;
let (_, pull_requests_list) = spin_until_ready(
ctx.client
.repo_list_pull_requests(
owner.as_str(),
repo.as_str(),
RepoListPullRequestsQuery::default(),
)
.send(),
)
.await
.into_diagnostic()?;
fuzzy_select_with_key(
&pull_requests_list,
select_prompt_for("pull request"),
display_pull_request,
)
.cloned()
}
async fn create_options(
ctx: &BergContext<EditPullRequestArgs>,
pull_request: &PullRequest,
) -> miette::Result<EditPullRequestOption> {
let selected_update_fields = multi_fuzzy_select_with_key(
EditableFields::VARIANTS,
select_prompt_for("options"),
|_| false,
|f| f.to_string(),
)?;
let mut options = EditPullRequestOption {
assignee: None,
assignees: None,
body: None,
due_date: None,
milestone: None,
state: None,
title: None,
unset_due_date: None,
allow_maintainer_edit: None,
base: None,
labels: None,
};
if selected_update_fields.contains(&&EditableFields::Assignees) {
let current_assignees = pull_request
.assignees
.as_ref()
.map(|users| users.iter().filter_map(|user| user.id).collect::<Vec<_>>());
options
.assignees
.replace(pull_request_assignees(ctx, current_assignees).await?);
}
if selected_update_fields.contains(&&EditableFields::Description) {
options
.body
.replace(pull_request_description(ctx, pull_request.body.clone()).await?);
}
if selected_update_fields.contains(&&EditableFields::State) {
options.state.replace(select_state(pull_request.state)?);
}
if selected_update_fields.contains(&&EditableFields::Title) {
options
.title
.replace(pull_request_title(ctx, pull_request.title.clone()).await?);
}
if selected_update_fields.contains(&&EditableFields::Labels) {
let current_labels = pull_request.labels.as_ref().map(|labels| {
labels
.iter()
.filter_map(|label| label.id)
.collect::<Vec<_>>()
});
options
.labels
.replace(pull_request_labels(ctx, current_labels).await?);
}
Ok(options)
}
async fn pull_request_assignees(
ctx: &BergContext<EditPullRequestArgs>,
current_assignees: Option<Vec<i64>>,
) -> miette::Result<Vec<String>> {
let OwnerRepo { repo, owner } = ctx.owner_repo()?;
let current_assignees = current_assignees.unwrap_or_default();
let (_, all_assignees) = ctx
.client
.repo_get_assignees(owner.as_str(), repo.as_str())
.await
.into_diagnostic()?;
let selected_assignees = multi_fuzzy_select_with_key(
&all_assignees,
select_prompt_for("assignees"),
|u| u.id.is_some_and(|id| current_assignees.contains(&id)),
|u| option_display(&u.login),
)?;
Ok(selected_assignees
.into_iter()
.filter_map(|u| u.login.as_ref().cloned())
.collect::<Vec<_>>())
}
async fn pull_request_description(
ctx: &BergContext<EditPullRequestArgs>,
current_description: Option<String>,
) -> miette::Result<String> {
ctx.editor_for(
"a description",
current_description
.as_deref()
.unwrap_or("Enter a pull request description"),
)
}
async fn pull_request_title(
_ctx: &BergContext<EditPullRequestArgs>,
current_title: Option<String>,
) -> miette::Result<String> {
inquire::Text::new(input_prompt_for("Choose a new pull request title").as_str())
.with_default(
current_title
.as_deref()
.unwrap_or("Enter pull request title"),
)
.prompt()
.into_diagnostic()
}
async fn pull_request_labels(
ctx: &BergContext<EditPullRequestArgs>,
current_labels: Option<Vec<i64>>,
) -> miette::Result<Vec<i64>> {
let OwnerRepo { repo, owner } = ctx.owner_repo()?;
let current_labels = current_labels.unwrap_or_default();
let (_, all_labels) = ctx
.client
.issue_list_labels(
owner.as_str(),
repo.as_str(),
IssueListLabelsQuery::default(),
)
.await
.into_diagnostic()?;
let selected_labels = multi_fuzzy_select_with_key(
&all_labels,
select_prompt_for("labels"),
|l| l.id.is_some_and(|id| current_labels.contains(&id)),
|l| option_display(&l.name),
)?;
let label_ids = selected_labels
.iter()
.filter_map(|l| l.id)
.collect::<Vec<_>>();
Ok(label_ids)
}