codeberg_cli/actions/pull_request/
edit.rs1use 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#[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}