codeberg_cli/actions/pull_request/
edit.rs1use 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, select_state};
6use crate::types::context::BergContext;
7use crate::types::git::OwnerRepo;
8use crate::types::output::OutputMode;
9use forgejo_api::structs::{
10 EditPullRequestOption, IssueListLabelsQuery, PullRequest, RepoListPullRequestsQuery,
11};
12use miette::{Context, IntoDiagnostic};
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#[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) -> miette::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 .into_diagnostic()?;
55
56 match ctx.global_args.output_mode {
57 OutputMode::Pretty => {
58 tracing::debug!("{updated_pull_request:?}");
59 }
60 OutputMode::Json => updated_pull_request.print_json()?,
61 }
62
63 Ok(())
64 }
65}
66
67async fn select_pull_request(
68 ctx: &BergContext<EditPullRequestArgs>,
69) -> miette::Result<PullRequest> {
70 let OwnerRepo { repo, owner } = ctx.owner_repo()?;
71 let (_, pull_requests_list) = spin_until_ready(
72 ctx.client
73 .repo_list_pull_requests(
74 owner.as_str(),
75 repo.as_str(),
76 RepoListPullRequestsQuery::default(),
77 )
78 .send(),
79 )
80 .await
81 .into_diagnostic()?;
82
83 fuzzy_select_with_key(
84 &pull_requests_list,
85 select_prompt_for("pull request"),
86 display_pull_request,
87 )
88 .cloned()
89}
90
91async fn create_options(
92 ctx: &BergContext<EditPullRequestArgs>,
93 pull_request: &PullRequest,
94) -> miette::Result<EditPullRequestOption> {
95 let selected_update_fields = multi_fuzzy_select_with_key(
96 EditableFields::VARIANTS,
97 select_prompt_for("options"),
98 |_| false,
99 |f| f.to_string(),
100 )?;
101
102 let mut options = EditPullRequestOption {
103 assignee: None,
104 assignees: None,
105 body: None,
106 due_date: None,
107 milestone: None,
108 state: None,
109 title: None,
110 unset_due_date: None,
111 allow_maintainer_edit: None,
112 base: None,
113 labels: None,
114 };
115
116 if selected_update_fields.contains(&&EditableFields::Assignees) {
117 let current_assignees = pull_request
118 .assignees
119 .as_ref()
120 .map(|users| users.iter().filter_map(|user| user.id).collect::<Vec<_>>());
121 options
122 .assignees
123 .replace(pull_request_assignees(ctx, current_assignees).await?);
124 }
125 if selected_update_fields.contains(&&EditableFields::Description) {
126 options
127 .body
128 .replace(pull_request_description(ctx, pull_request.body.clone()).await?);
129 }
130 if selected_update_fields.contains(&&EditableFields::State) {
131 options.state.replace(select_state(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) -> miette::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 .into_diagnostic()?;
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) -> miette::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
189async fn pull_request_title(
190 _ctx: &BergContext<EditPullRequestArgs>,
191 current_title: Option<String>,
192) -> miette::Result<String> {
193 inquire::Text::new(input_prompt_for("Choose a new pull request title").as_str())
194 .with_default(
195 current_title
196 .as_deref()
197 .unwrap_or("Enter pull request title"),
198 )
199 .prompt()
200 .into_diagnostic()
201}
202
203async fn pull_request_labels(
204 ctx: &BergContext<EditPullRequestArgs>,
205 current_labels: Option<Vec<i64>>,
206) -> miette::Result<Vec<i64>> {
207 let OwnerRepo { repo, owner } = ctx.owner_repo()?;
208 let current_labels = current_labels.unwrap_or_default();
209 let (_, all_labels) = ctx
210 .client
211 .issue_list_labels(
212 owner.as_str(),
213 repo.as_str(),
214 IssueListLabelsQuery::default(),
215 )
216 .await
217 .into_diagnostic()?;
218
219 let selected_labels = multi_fuzzy_select_with_key(
220 &all_labels,
221 select_prompt_for("labels"),
222 |l| l.id.is_some_and(|id| current_labels.contains(&id)),
223 |l| option_display(&l.name),
224 )?;
225
226 let label_ids = selected_labels
227 .iter()
228 .filter_map(|l| l.id)
229 .collect::<Vec<_>>();
230
231 Ok(label_ids)
232}