codeberg_cli/actions/pull_request/
edit.rs

1use crate::actions::GeneralArgs;
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, general_args: GeneralArgs) -> anyhow::Result<()> {
36        let ctx = BergContext::new(self, general_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(
53                owner.as_str(),
54                repo.as_str(),
55                pull_request_id as u64,
56                options,
57            )
58            .await?;
59
60        match general_args.output_mode {
61            OutputMode::Pretty => {
62                tracing::debug!("{updated_pull_request:?}");
63            }
64            OutputMode::Json => updated_pull_request.print_json()?,
65        }
66
67        Ok(())
68    }
69}
70
71async fn select_pull_request(
72    ctx: &BergContext<EditPullRequestArgs>,
73) -> anyhow::Result<PullRequest> {
74    let OwnerRepo { repo, owner } = ctx.owner_repo()?;
75    let (_, pull_requests_list) = spin_until_ready(ctx.client.repo_list_pull_requests(
76        owner.as_str(),
77        repo.as_str(),
78        RepoListPullRequestsQuery::default(),
79    ))
80    .await?;
81
82    fuzzy_select_with_key(
83        &pull_requests_list,
84        select_prompt_for("pull request"),
85        display_pull_request,
86    )
87    .cloned()
88}
89
90async fn create_options(
91    ctx: &BergContext<EditPullRequestArgs>,
92    pull_request: &PullRequest,
93) -> anyhow::Result<EditPullRequestOption> {
94    let selected_update_fields = multi_fuzzy_select_with_key(
95        EditableFields::VARIANTS,
96        select_prompt_for("options"),
97        |_| false,
98        |f| f.to_string(),
99    )?;
100
101    let mut options = EditPullRequestOption {
102        assignee: None,
103        assignees: None,
104        body: None,
105        due_date: None,
106        milestone: None,
107        state: None,
108        title: None,
109        unset_due_date: None,
110        allow_maintainer_edit: None,
111        base: None,
112        labels: None,
113    };
114
115    if selected_update_fields.contains(&&EditableFields::Assignees) {
116        let current_assignees = pull_request
117            .assignees
118            .as_ref()
119            .map(|users| users.iter().filter_map(|user| user.id).collect::<Vec<_>>());
120        options
121            .assignees
122            .replace(pull_request_assignees(ctx, current_assignees).await?);
123    }
124    if selected_update_fields.contains(&&EditableFields::Description) {
125        options
126            .body
127            .replace(pull_request_description(ctx, pull_request.body.clone()).await?);
128    }
129    if selected_update_fields.contains(&&EditableFields::State) {
130        options
131            .state
132            .replace(pull_request_state(ctx, pull_request.state)?);
133    }
134    if selected_update_fields.contains(&&EditableFields::Title) {
135        options
136            .title
137            .replace(pull_request_title(ctx, pull_request.title.clone()).await?);
138    }
139    if selected_update_fields.contains(&&EditableFields::Labels) {
140        let current_labels = pull_request.labels.as_ref().map(|labels| {
141            labels
142                .iter()
143                .filter_map(|label| label.id)
144                .collect::<Vec<_>>()
145        });
146        options
147            .labels
148            .replace(pull_request_labels(ctx, current_labels).await?);
149    }
150
151    Ok(options)
152}
153
154async fn pull_request_assignees(
155    ctx: &BergContext<EditPullRequestArgs>,
156    current_assignees: Option<Vec<i64>>,
157) -> anyhow::Result<Vec<String>> {
158    let OwnerRepo { repo, owner } = ctx.owner_repo()?;
159    let current_assignees = current_assignees.unwrap_or_default();
160    let (_, all_assignees) = ctx
161        .client
162        .repo_get_assignees(owner.as_str(), repo.as_str())
163        .await?;
164    let selected_assignees = multi_fuzzy_select_with_key(
165        &all_assignees,
166        select_prompt_for("assignees"),
167        |u| u.id.is_some_and(|id| current_assignees.contains(&id)),
168        |u| option_display(&u.login),
169    )?;
170
171    Ok(selected_assignees
172        .into_iter()
173        .filter_map(|u| u.login.as_ref().cloned())
174        .collect::<Vec<_>>())
175}
176
177async fn pull_request_description(
178    ctx: &BergContext<EditPullRequestArgs>,
179    current_description: Option<String>,
180) -> anyhow::Result<String> {
181    ctx.editor_for(
182        "a description",
183        current_description
184            .as_deref()
185            .unwrap_or("Enter a pull request description"),
186    )
187}
188
189fn pull_request_state(
190    _ctx: &BergContext<EditPullRequestArgs>,
191    _current_state: Option<StateType>,
192) -> anyhow::Result<String> {
193    let selected_state = fuzzy_select_with_key(
194        &[StateType::Open, StateType::Closed],
195        select_prompt_for("states"),
196        |f| format!("{f:?}"),
197    )?;
198    Ok(format!("{selected_state:?}"))
199}
200
201async fn pull_request_title(
202    _ctx: &BergContext<EditPullRequestArgs>,
203    current_title: Option<String>,
204) -> anyhow::Result<String> {
205    inquire::Text::new(input_prompt_for("Choose a new pull request title").as_str())
206        .with_default(
207            current_title
208                .as_deref()
209                .unwrap_or("Enter pull request title"),
210        )
211        .prompt()
212        .map_err(anyhow::Error::from)
213}
214
215async fn pull_request_labels(
216    ctx: &BergContext<EditPullRequestArgs>,
217    current_labels: Option<Vec<i64>>,
218) -> anyhow::Result<Vec<i64>> {
219    let OwnerRepo { repo, owner } = ctx.owner_repo()?;
220    let current_labels = current_labels.unwrap_or_default();
221    let (_, all_labels) = ctx
222        .client
223        .issue_list_labels(
224            owner.as_str(),
225            repo.as_str(),
226            IssueListLabelsQuery::default(),
227        )
228        .await?;
229
230    let selected_labels = multi_fuzzy_select_with_key(
231        &all_labels,
232        select_prompt_for("labels"),
233        |l| l.id.is_some_and(|id| current_labels.contains(&id)),
234        |l| option_display(&l.name),
235    )?;
236
237    let label_ids = selected_labels
238        .iter()
239        .filter_map(|l| l.id)
240        .collect::<Vec<_>>();
241
242    Ok(label_ids)
243}