codeberg_cli/actions/pull_request/
edit.rs

1use crate::actions::GlobalArgs;
2use crate::render::json::JsonToStdout;
3use crate::render::option::option_display;
4use crate::render::spinner::spin_until_ready;
5use crate::render::ui::{fuzzy_select_with_key, multi_fuzzy_select_with_key};
6use crate::types::context::BergContext;
7use crate::types::git::OwnerRepo;
8use crate::types::output::OutputMode;
9use anyhow::Context;
10use forgejo_api::structs::{
11    EditPullRequestOption, IssueListLabelsQuery, PullRequest, RepoListPullRequestsQuery, StateType,
12};
13use strum::{Display, VariantArray};
14
15use crate::actions::text_manipulation::{input_prompt_for, select_prompt_for};
16
17use super::display_pull_request;
18
19use clap::Parser;
20
21/// Edit pull request
22#[derive(Parser, Debug)]
23pub struct EditPullRequestArgs {}
24
25#[derive(Display, PartialEq, Eq, VariantArray)]
26enum EditableFields {
27    Assignees,
28    Description,
29    State,
30    Labels,
31    Title,
32}
33
34impl EditPullRequestArgs {
35    pub async fn run(self, global_args: GlobalArgs) -> anyhow::Result<()> {
36        let ctx = BergContext::new(self, global_args).await?;
37
38        let OwnerRepo { repo, owner } = ctx.owner_repo()?;
39        let pull_request = select_pull_request(&ctx).await?;
40        let pull_request_id = pull_request
41            .number
42            .context("Selected pull_request doesn't have an ID")?;
43
44        let options = create_options(&ctx, &pull_request).await?;
45
46        tracing::debug!("{options:?}");
47        tracing::debug!("{owner:?}/{repo:?}");
48        tracing::debug!("{pull_request_id:?}");
49
50        let updated_pull_request = ctx
51            .client
52            .repo_edit_pull_request(owner.as_str(), repo.as_str(), pull_request_id, options)
53            .await?;
54
55        match ctx.global_args.output_mode {
56            OutputMode::Pretty => {
57                tracing::debug!("{updated_pull_request:?}");
58            }
59            OutputMode::Json => updated_pull_request.print_json()?,
60        }
61
62        Ok(())
63    }
64}
65
66async fn select_pull_request(
67    ctx: &BergContext<EditPullRequestArgs>,
68) -> anyhow::Result<PullRequest> {
69    let OwnerRepo { repo, owner } = ctx.owner_repo()?;
70    let (_, pull_requests_list) = spin_until_ready(
71        ctx.client
72            .repo_list_pull_requests(
73                owner.as_str(),
74                repo.as_str(),
75                RepoListPullRequestsQuery::default(),
76            )
77            .send(),
78    )
79    .await?;
80
81    fuzzy_select_with_key(
82        &pull_requests_list,
83        select_prompt_for("pull request"),
84        display_pull_request,
85    )
86    .cloned()
87}
88
89async fn create_options(
90    ctx: &BergContext<EditPullRequestArgs>,
91    pull_request: &PullRequest,
92) -> anyhow::Result<EditPullRequestOption> {
93    let selected_update_fields = multi_fuzzy_select_with_key(
94        EditableFields::VARIANTS,
95        select_prompt_for("options"),
96        |_| false,
97        |f| f.to_string(),
98    )?;
99
100    let mut options = EditPullRequestOption {
101        assignee: None,
102        assignees: None,
103        body: None,
104        due_date: None,
105        milestone: None,
106        state: None,
107        title: None,
108        unset_due_date: None,
109        allow_maintainer_edit: None,
110        base: None,
111        labels: None,
112    };
113
114    if selected_update_fields.contains(&&EditableFields::Assignees) {
115        let current_assignees = pull_request
116            .assignees
117            .as_ref()
118            .map(|users| users.iter().filter_map(|user| user.id).collect::<Vec<_>>());
119        options
120            .assignees
121            .replace(pull_request_assignees(ctx, current_assignees).await?);
122    }
123    if selected_update_fields.contains(&&EditableFields::Description) {
124        options
125            .body
126            .replace(pull_request_description(ctx, pull_request.body.clone()).await?);
127    }
128    if selected_update_fields.contains(&&EditableFields::State) {
129        options
130            .state
131            .replace(pull_request_state(ctx, pull_request.state)?);
132    }
133    if selected_update_fields.contains(&&EditableFields::Title) {
134        options
135            .title
136            .replace(pull_request_title(ctx, pull_request.title.clone()).await?);
137    }
138    if selected_update_fields.contains(&&EditableFields::Labels) {
139        let current_labels = pull_request.labels.as_ref().map(|labels| {
140            labels
141                .iter()
142                .filter_map(|label| label.id)
143                .collect::<Vec<_>>()
144        });
145        options
146            .labels
147            .replace(pull_request_labels(ctx, current_labels).await?);
148    }
149
150    Ok(options)
151}
152
153async fn pull_request_assignees(
154    ctx: &BergContext<EditPullRequestArgs>,
155    current_assignees: Option<Vec<i64>>,
156) -> anyhow::Result<Vec<String>> {
157    let OwnerRepo { repo, owner } = ctx.owner_repo()?;
158    let current_assignees = current_assignees.unwrap_or_default();
159    let (_, all_assignees) = ctx
160        .client
161        .repo_get_assignees(owner.as_str(), repo.as_str())
162        .await?;
163    let selected_assignees = multi_fuzzy_select_with_key(
164        &all_assignees,
165        select_prompt_for("assignees"),
166        |u| u.id.is_some_and(|id| current_assignees.contains(&id)),
167        |u| option_display(&u.login),
168    )?;
169
170    Ok(selected_assignees
171        .into_iter()
172        .filter_map(|u| u.login.as_ref().cloned())
173        .collect::<Vec<_>>())
174}
175
176async fn pull_request_description(
177    ctx: &BergContext<EditPullRequestArgs>,
178    current_description: Option<String>,
179) -> anyhow::Result<String> {
180    ctx.editor_for(
181        "a description",
182        current_description
183            .as_deref()
184            .unwrap_or("Enter a pull request description"),
185    )
186}
187
188fn pull_request_state(
189    _ctx: &BergContext<EditPullRequestArgs>,
190    _current_state: Option<StateType>,
191) -> anyhow::Result<String> {
192    let selected_state = fuzzy_select_with_key(
193        &[StateType::Open, StateType::Closed],
194        select_prompt_for("states"),
195        |f| format!("{f:?}"),
196    )?;
197    Ok(format!("{selected_state:?}"))
198}
199
200async fn pull_request_title(
201    _ctx: &BergContext<EditPullRequestArgs>,
202    current_title: Option<String>,
203) -> anyhow::Result<String> {
204    inquire::Text::new(input_prompt_for("Choose a new pull request title").as_str())
205        .with_default(
206            current_title
207                .as_deref()
208                .unwrap_or("Enter pull request title"),
209        )
210        .prompt()
211        .map_err(anyhow::Error::from)
212}
213
214async fn pull_request_labels(
215    ctx: &BergContext<EditPullRequestArgs>,
216    current_labels: Option<Vec<i64>>,
217) -> anyhow::Result<Vec<i64>> {
218    let OwnerRepo { repo, owner } = ctx.owner_repo()?;
219    let current_labels = current_labels.unwrap_or_default();
220    let (_, all_labels) = ctx
221        .client
222        .issue_list_labels(
223            owner.as_str(),
224            repo.as_str(),
225            IssueListLabelsQuery::default(),
226        )
227        .await?;
228
229    let selected_labels = multi_fuzzy_select_with_key(
230        &all_labels,
231        select_prompt_for("labels"),
232        |l| l.id.is_some_and(|id| current_labels.contains(&id)),
233        |l| option_display(&l.name),
234    )?;
235
236    let label_ids = selected_labels
237        .iter()
238        .filter_map(|l| l.id)
239        .collect::<Vec<_>>();
240
241    Ok(label_ids)
242}