codeberg_cli/actions/pull_request/
edit.rs

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